From 771227ecf91dae46c8108836ecaf20737820e6f6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 6 Mar 2026 15:02:04 -0500 Subject: [PATCH 001/111] =?UTF-8?q?=F0=9F=8F=8E=EF=B8=8F=20refactor:=20Rep?= =?UTF-8?q?lace=20Sandpack=20Code=20Editor=20with=20Monaco=20for=20Artifac?= =?UTF-8?q?t=20Editing=20(#12109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Code Editor and Auto Scroll Functionality - Added a useEffect hook in CodeEditor to sync streaming content with Sandpack without remounting the provider, improving performance and user experience. - Updated useAutoScroll to accept an optional editorRef, allowing for dynamic scroll container selection based on the editor's state. - Refactored ArtifactTabs to utilize the new editorRef in the useAutoScroll hook, ensuring consistent scrolling behavior during content updates. - Introduced stableFiles and mergedFiles logic in CodeEditor to optimize file handling and prevent unnecessary updates during streaming content changes. * refactor: Update CodeEditor to Sync Streaming Content Based on Read-Only State - Modified the useEffect hook in CodeEditor to conditionally sync streaming content with Sandpack only when in read-only mode, preventing unnecessary updates during user edits. - Enhanced the dependency array of the useEffect hook to include the readOnly state, ensuring accurate synchronization behavior. * refactor: Monaco Editor for Artifact Code Editing * refactor: Clean up ArtifactCodeEditor and ArtifactTabs components - Removed unused scrollbar styles from mobile.css to streamline the code. - Refactored ArtifactCodeEditor to improve content synchronization and read-only state handling. - Enhanced ArtifactTabs by removing unnecessary context usage and optimizing component structure for better readability. * feat: Add support for new artifact type 'application/vnd.ant.react' - Introduced handling for 'application/vnd.ant.react' in artifactFilename, artifactTemplate, and dependenciesMap. - Updated relevant mappings to ensure proper integration of the new artifact type within the application. * refactor:ArtifactCodeEditor with Monaco Editor Configuration - Added support for disabling validation in the Monaco Editor to improve the artifact viewer/editor experience. - Introduced a new type definition for Monaco to enhance type safety. - Updated the handling of the 'application/vnd.ant.react' artifact type to ensure proper integration with the editor. * refactor: Clean up ArtifactCodeEditor and mobile.css - Removed unnecessary whitespace in mobile.css for cleaner code. - Refactored ArtifactCodeEditor to streamline language mapping and type handling, enhancing readability and maintainability. - Consolidated language and type mappings into dedicated constants for improved clarity and efficiency. * feat: Integrate Monaco Editor for Enhanced Code Editing Experience - Added the Monaco Editor as a dependency to improve the code editing capabilities within the ArtifactCodeEditor component. - Refactored the handling of TypeScript and JavaScript defaults in the Monaco Editor configuration for better type safety and clarity. - Streamlined the setup for disabling validation, enhancing the artifact viewer/editor experience. * fix: Update ArtifactCodeEditor to handle null content checks - Modified conditional checks in ArtifactCodeEditor to use `art.content != null` instead of `art.content` for improved null safety. - Ensured consistent handling of artifact content across various useEffect hooks to prevent potential errors when content is null. * fix: Refine content comparison logic in ArtifactCodeEditor - Updated the condition for checking if the code is not original by removing the redundant null check for `art.content`, ensuring more concise and clear logic. - This change enhances the readability of the code and maintains the integrity of content comparison within the editor. * fix: Simplify code comparison logic in ArtifactCodeEditor - Removed redundant null check for the `code` variable, ensuring a more straightforward comparison with the current update reference. - This change improves code clarity and maintains the integrity of the content comparison logic within the editor. --- client/package.json | 2 + .../Artifacts/ArtifactCodeEditor.tsx | 490 +++++++++++------- .../src/components/Artifacts/ArtifactTabs.tsx | 29 +- client/src/components/Artifacts/Artifacts.tsx | 4 +- client/src/components/Artifacts/Code.tsx | 75 +-- client/src/hooks/Artifacts/useAutoScroll.ts | 47 -- client/src/mobile.css | 20 - client/src/utils/artifacts.ts | 3 + package-lock.json | 64 +++ 9 files changed, 385 insertions(+), 349 deletions(-) delete mode 100644 client/src/hooks/Artifacts/useAutoScroll.ts diff --git a/client/package.json b/client/package.json index 0f7ffed04c..31e75c4702 100644 --- a/client/package.json +++ b/client/package.json @@ -38,6 +38,7 @@ "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@mcp-ui/client": "^5.7.0", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", @@ -146,6 +147,7 @@ "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^30.2.0", + "monaco-editor": "^0.55.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", "postcss": "^8.4.31", diff --git a/client/src/components/Artifacts/ArtifactCodeEditor.tsx b/client/src/components/Artifacts/ArtifactCodeEditor.tsx index 4ab2b182b8..ddf8dc84fa 100644 --- a/client/src/components/Artifacts/ArtifactCodeEditor.tsx +++ b/client/src/components/Artifacts/ArtifactCodeEditor.tsx @@ -1,206 +1,326 @@ -import React, { useMemo, useState, useEffect, useRef, memo } from 'react'; +import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react'; import debounce from 'lodash/debounce'; -import { KeyBinding } from '@codemirror/view'; -import { autocompletion, completionKeymap } from '@codemirror/autocomplete'; -import { - useSandpack, - SandpackCodeEditor, - SandpackProvider as StyledProvider, -} from '@codesandbox/sandpack-react'; -import type { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled'; -import type { SandpackBundlerFile } from '@codesandbox/sandpack-client'; -import type { CodeEditorRef } from '@codesandbox/sandpack-react'; -import type { ArtifactFiles, Artifact } from '~/common'; -import { useEditArtifact, useGetStartupConfig } from '~/data-provider'; +import MonacoEditor from '@monaco-editor/react'; +import type { Monaco } from '@monaco-editor/react'; +import type { editor } from 'monaco-editor'; +import type { Artifact } from '~/common'; import { useMutationState, useCodeState } from '~/Providers/EditorContext'; import { useArtifactsContext } from '~/Providers'; -import { sharedFiles, sharedOptions } from '~/utils/artifacts'; +import { useEditArtifact } from '~/data-provider'; -const CodeEditor = memo( - ({ - fileKey, - readOnly, - artifact, - editorRef, - }: { - fileKey: string; - readOnly?: boolean; - artifact: Artifact; - editorRef: React.MutableRefObject; - }) => { - const { sandpack } = useSandpack(); - const [currentUpdate, setCurrentUpdate] = useState(null); - const { isMutating, setIsMutating } = useMutationState(); - const { setCurrentCode } = useCodeState(); - const editArtifact = useEditArtifact({ - onMutate: (vars) => { - setIsMutating(true); - setCurrentUpdate(vars.updated); - }, - onSuccess: () => { - setIsMutating(false); - setCurrentUpdate(null); - }, - onError: () => { - setIsMutating(false); - }, - }); +const LANG_MAP: Record = { + javascript: 'javascript', + typescript: 'typescript', + python: 'python', + css: 'css', + json: 'json', + markdown: 'markdown', + html: 'html', + xml: 'xml', + sql: 'sql', + yaml: 'yaml', + shell: 'shell', + bash: 'shell', + tsx: 'typescript', + jsx: 'javascript', + c: 'c', + cpp: 'cpp', + java: 'java', + go: 'go', + rust: 'rust', + kotlin: 'kotlin', + swift: 'swift', + php: 'php', + ruby: 'ruby', + r: 'r', + lua: 'lua', + scala: 'scala', + perl: 'perl', +}; - /** - * Create stable debounced mutation that doesn't depend on changing callbacks - * Use refs to always access the latest values without recreating the debounce - */ - const artifactRef = useRef(artifact); - const isMutatingRef = useRef(isMutating); - const currentUpdateRef = useRef(currentUpdate); - const editArtifactRef = useRef(editArtifact); - const setCurrentCodeRef = useRef(setCurrentCode); +const TYPE_MAP: Record = { + 'text/html': 'html', + 'application/vnd.code-html': 'html', + 'application/vnd.react': 'typescript', + 'application/vnd.ant.react': 'typescript', + 'text/markdown': 'markdown', + 'text/md': 'markdown', + 'text/plain': 'plaintext', + 'application/vnd.mermaid': 'markdown', +}; - useEffect(() => { - artifactRef.current = artifact; - }, [artifact]); +function getMonacoLanguage(type?: string, language?: string): string { + if (language && LANG_MAP[language]) { + return LANG_MAP[language]; + } + return TYPE_MAP[type ?? ''] ?? 'plaintext'; +} - useEffect(() => { - isMutatingRef.current = isMutating; - }, [isMutating]); - - useEffect(() => { - currentUpdateRef.current = currentUpdate; - }, [currentUpdate]); - - useEffect(() => { - editArtifactRef.current = editArtifact; - }, [editArtifact]); - - useEffect(() => { - setCurrentCodeRef.current = setCurrentCode; - }, [setCurrentCode]); - - /** - * Create debounced mutation once - never recreate it - * All values are accessed via refs so they're always current - */ - const debouncedMutation = useMemo( - () => - debounce((code: string) => { - if (readOnly) { - return; - } - if (isMutatingRef.current) { - return; - } - if (artifactRef.current.index == null) { - return; - } - - const artifact = artifactRef.current; - const artifactIndex = artifact.index; - const isNotOriginal = - code && artifact.content != null && code.trim() !== artifact.content.trim(); - const isNotRepeated = - currentUpdateRef.current == null - ? true - : code != null && code.trim() !== currentUpdateRef.current.trim(); - - if (artifact.content && isNotOriginal && isNotRepeated && artifactIndex != null) { - setCurrentCodeRef.current(code); - editArtifactRef.current.mutate({ - index: artifactIndex, - messageId: artifact.messageId ?? '', - original: artifact.content, - updated: code, - }); - } - }, 500), - [readOnly], - ); - - /** - * Listen to Sandpack file changes and trigger debounced mutation - */ - useEffect(() => { - const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code; - if (currentCode) { - debouncedMutation(currentCode); - } - }, [sandpack.files, fileKey, debouncedMutation]); - - /** - * Cleanup: cancel pending mutations when component unmounts or artifact changes - */ - useEffect(() => { - return () => { - debouncedMutation.cancel(); - }; - }, [artifact.id, debouncedMutation]); - - return ( - (completionKeymap)} - className="hljs language-javascript bg-black" - /> - ); - }, -); - -export const ArtifactCodeEditor = function ({ - files, - fileKey, - template, +export const ArtifactCodeEditor = function ArtifactCodeEditor({ artifact, - editorRef, - sharedProps, + monacoRef, readOnly: externalReadOnly, }: { - fileKey: string; artifact: Artifact; - files: ArtifactFiles; - template: SandpackProviderProps['template']; - sharedProps: Partial; - editorRef: React.MutableRefObject; + monacoRef: React.MutableRefObject; readOnly?: boolean; }) { - const { data: config } = useGetStartupConfig(); const { isSubmitting } = useArtifactsContext(); - const options: typeof sharedOptions = useMemo(() => { - if (!config) { - return sharedOptions; - } - return { - ...sharedOptions, - activeFile: '/' + fileKey, - bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL, - }; - }, [config, template, fileKey]); - const initialReadOnly = (externalReadOnly ?? false) || (isSubmitting ?? false); - const [readOnly, setReadOnly] = useState(initialReadOnly); - useEffect(() => { - setReadOnly((externalReadOnly ?? false) || (isSubmitting ?? false)); - }, [isSubmitting, externalReadOnly]); + const readOnly = (externalReadOnly ?? false) || isSubmitting; + const { setCurrentCode } = useCodeState(); + const [currentUpdate, setCurrentUpdate] = useState(null); + const { isMutating, setIsMutating } = useMutationState(); + const editArtifact = useEditArtifact({ + onMutate: (vars) => { + setIsMutating(true); + setCurrentUpdate(vars.updated); + }, + onSuccess: () => { + setIsMutating(false); + setCurrentUpdate(null); + }, + onError: () => { + setIsMutating(false); + }, + }); - if (Object.keys(files).length === 0) { + const artifactRef = useRef(artifact); + const isMutatingRef = useRef(isMutating); + const currentUpdateRef = useRef(currentUpdate); + const editArtifactRef = useRef(editArtifact); + const setCurrentCodeRef = useRef(setCurrentCode); + const prevContentRef = useRef(artifact.content ?? ''); + const prevArtifactId = useRef(artifact.id); + const prevReadOnly = useRef(readOnly); + + artifactRef.current = artifact; + isMutatingRef.current = isMutating; + currentUpdateRef.current = currentUpdate; + editArtifactRef.current = editArtifact; + setCurrentCodeRef.current = setCurrentCode; + + const debouncedMutation = useMemo( + () => + debounce((code: string) => { + if (readOnly || isMutatingRef.current || artifactRef.current.index == null) { + return; + } + const art = artifactRef.current; + const isNotOriginal = art.content != null && code.trim() !== art.content.trim(); + const isNotRepeated = + currentUpdateRef.current == null ? true : code.trim() !== currentUpdateRef.current.trim(); + + if (art.content != null && isNotOriginal && isNotRepeated && art.index != null) { + setCurrentCodeRef.current(code); + editArtifactRef.current.mutate({ + index: art.index, + messageId: art.messageId ?? '', + original: art.content, + updated: code, + }); + } + }, 500), + [readOnly], + ); + + useEffect(() => { + return () => debouncedMutation.cancel(); + }, [artifact.id, debouncedMutation]); + + /** + * Streaming: use model.applyEdits() to append new content. + * Unlike setValue/pushEditOperations, applyEdits preserves existing + * tokens so syntax highlighting doesn't flash during updates. + */ + useEffect(() => { + const ed = monacoRef.current; + if (!ed || !readOnly) { + return; + } + const newContent = artifact.content ?? ''; + const prev = prevContentRef.current; + + if (newContent === prev) { + return; + } + + const model = ed.getModel(); + if (!model) { + return; + } + + if (newContent.startsWith(prev) && prev.length > 0) { + const appended = newContent.slice(prev.length); + const endPos = model.getPositionAt(model.getValueLength()); + model.applyEdits([ + { + range: { + startLineNumber: endPos.lineNumber, + startColumn: endPos.column, + endLineNumber: endPos.lineNumber, + endColumn: endPos.column, + }, + text: appended, + }, + ]); + } else { + model.setValue(newContent); + } + + prevContentRef.current = newContent; + ed.revealLine(model.getLineCount()); + }, [artifact.content, readOnly, monacoRef]); + + useEffect(() => { + if (artifact.id === prevArtifactId.current) { + return; + } + prevArtifactId.current = artifact.id; + prevContentRef.current = artifact.content ?? ''; + const ed = monacoRef.current; + if (ed && artifact.content != null) { + ed.getModel()?.setValue(artifact.content); + } + }, [artifact.id, artifact.content, monacoRef]); + + useEffect(() => { + if (prevReadOnly.current && !readOnly && artifact.content != null) { + const ed = monacoRef.current; + if (ed) { + ed.getModel()?.setValue(artifact.content); + prevContentRef.current = artifact.content; + } + } + prevReadOnly.current = readOnly; + }, [readOnly, artifact.content, monacoRef]); + + const handleChange = useCallback( + (value: string | undefined) => { + if (value === undefined || readOnly) { + return; + } + prevContentRef.current = value; + setCurrentCode(value); + if (value.length > 0) { + debouncedMutation(value); + } + }, + [readOnly, debouncedMutation, setCurrentCode], + ); + + /** + * Disable all validation — this is an artifact viewer/editor, not an IDE. + * Note: these are global Monaco settings that affect all editor instances on the page. + * The `as unknown` cast is required because monaco-editor v0.55 types `.languages.typescript` + * as `{ deprecated: true }` while the runtime API is fully functional. + */ + const handleBeforeMount = useCallback((monaco: Monaco) => { + const { typescriptDefaults, javascriptDefaults, JsxEmit } = monaco.languages + .typescript as unknown as { + typescriptDefaults: { + setDiagnosticsOptions: (o: { + noSemanticValidation: boolean; + noSyntaxValidation: boolean; + }) => void; + setCompilerOptions: (o: { + allowNonTsExtensions: boolean; + allowJs: boolean; + jsx: number; + }) => void; + }; + javascriptDefaults: { + setDiagnosticsOptions: (o: { + noSemanticValidation: boolean; + noSyntaxValidation: boolean; + }) => void; + setCompilerOptions: (o: { + allowNonTsExtensions: boolean; + allowJs: boolean; + jsx: number; + }) => void; + }; + JsxEmit: { React: number }; + }; + const diagnosticsOff = { noSemanticValidation: true, noSyntaxValidation: true }; + const compilerBase = { allowNonTsExtensions: true, allowJs: true, jsx: JsxEmit.React }; + typescriptDefaults.setDiagnosticsOptions(diagnosticsOff); + javascriptDefaults.setDiagnosticsOptions(diagnosticsOff); + typescriptDefaults.setCompilerOptions(compilerBase); + javascriptDefaults.setCompilerOptions(compilerBase); + }, []); + + const handleMount = useCallback( + (ed: editor.IStandaloneCodeEditor) => { + monacoRef.current = ed; + prevContentRef.current = ed.getModel()?.getValue() ?? artifact.content ?? ''; + if (readOnly) { + const model = ed.getModel(); + if (model) { + ed.revealLine(model.getLineCount()); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [monacoRef], + ); + + const language = getMonacoLanguage(artifact.type, artifact.language); + + const editorOptions = useMemo( + () => ({ + readOnly, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 13, + tabSize: 2, + wordWrap: 'on', + automaticLayout: true, + padding: { top: 8 }, + renderLineHighlight: readOnly ? 'none' : 'line', + cursorStyle: readOnly ? 'underline-thin' : 'line', + scrollbar: { + vertical: 'visible', + horizontal: 'auto', + verticalScrollbarSize: 8, + horizontalScrollbarSize: 8, + useShadows: false, + alwaysConsumeMouseWheel: false, + }, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + folding: false, + glyphMargin: false, + colorDecorators: !readOnly, + occurrencesHighlight: readOnly ? 'off' : 'singleFile', + selectionHighlight: !readOnly, + renderValidationDecorations: readOnly ? 'off' : 'editable', + quickSuggestions: !readOnly, + suggestOnTriggerCharacters: !readOnly, + parameterHints: { enabled: !readOnly }, + hover: { enabled: !readOnly }, + matchBrackets: readOnly ? 'never' : 'always', + }), + [readOnly], + ); + + if (!artifact.content) { return null; } return ( - - - +
+ +
); }; diff --git a/client/src/components/Artifacts/ArtifactTabs.tsx b/client/src/components/Artifacts/ArtifactTabs.tsx index 8e2a92eb9c..32332215f0 100644 --- a/client/src/components/Artifacts/ArtifactTabs.tsx +++ b/client/src/components/Artifacts/ArtifactTabs.tsx @@ -1,30 +1,26 @@ import { useRef, useEffect } from 'react'; import * as Tabs from '@radix-ui/react-tabs'; import type { SandpackPreviewRef } from '@codesandbox/sandpack-react/unstyled'; -import type { CodeEditorRef } from '@codesandbox/sandpack-react'; +import type { editor } from 'monaco-editor'; import type { Artifact } from '~/common'; import { useCodeState } from '~/Providers/EditorContext'; -import { useArtifactsContext } from '~/Providers'; import useArtifactProps from '~/hooks/Artifacts/useArtifactProps'; -import { useAutoScroll } from '~/hooks/Artifacts/useAutoScroll'; import { ArtifactCodeEditor } from './ArtifactCodeEditor'; import { useGetStartupConfig } from '~/data-provider'; import { ArtifactPreview } from './ArtifactPreview'; export default function ArtifactTabs({ artifact, - editorRef, previewRef, isSharedConvo, }: { artifact: Artifact; - editorRef: React.MutableRefObject; previewRef: React.MutableRefObject; isSharedConvo?: boolean; }) { - const { isSubmitting } = useArtifactsContext(); const { currentCode, setCurrentCode } = useCodeState(); const { data: startupConfig } = useGetStartupConfig(); + const monacoRef = useRef(null); const lastIdRef = useRef(null); useEffect(() => { @@ -34,33 +30,24 @@ export default function ArtifactTabs({ lastIdRef.current = artifact.id; }, [setCurrentCode, artifact.id]); - const content = artifact.content ?? ''; - const contentRef = useRef(null); - useAutoScroll({ ref: contentRef, content, isSubmitting }); - const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact }); return (
- + - + (); const previewRef = useRef(); const [isVisible, setIsVisible] = useState(false); const [isClosing, setIsClosing] = useState(false); @@ -297,7 +296,6 @@ export default function Artifacts() {
} previewRef={previewRef as React.MutableRefObject} isSharedConvo={isSharedConvo} /> diff --git a/client/src/components/Artifacts/Code.tsx b/client/src/components/Artifacts/Code.tsx index 6894ce775b..001b010908 100644 --- a/client/src/components/Artifacts/Code.tsx +++ b/client/src/components/Artifacts/Code.tsx @@ -1,11 +1,8 @@ -import React, { memo, useEffect, useRef, useState } from 'react'; +import React, { memo, useState } from 'react'; import copy from 'copy-to-clipboard'; -import rehypeKatex from 'rehype-katex'; -import ReactMarkdown from 'react-markdown'; import { Button } from '@librechat/client'; -import rehypeHighlight from 'rehype-highlight'; import { Copy, CircleCheckBig } from 'lucide-react'; -import { handleDoubleClick, langSubset } from '~/utils'; +import { handleDoubleClick } from '~/utils'; import { useLocalize } from '~/hooks'; type TCodeProps = { @@ -29,74 +26,6 @@ export const code: React.ElementType = memo(({ inline, className, children }: TC return {children}; }); -export const CodeMarkdown = memo( - ({ content = '', isSubmitting }: { content: string; isSubmitting: boolean }) => { - const scrollRef = useRef(null); - const [userScrolled, setUserScrolled] = useState(false); - const currentContent = content; - const rehypePlugins = [ - [rehypeKatex], - [ - rehypeHighlight, - { - detect: true, - ignoreMissing: true, - subset: langSubset, - }, - ], - ]; - - useEffect(() => { - const scrollContainer = scrollRef.current; - if (!scrollContainer) { - return; - } - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = scrollContainer; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; - - if (!isNearBottom) { - setUserScrolled(true); - } else { - setUserScrolled(false); - } - }; - - scrollContainer.addEventListener('scroll', handleScroll); - - return () => { - scrollContainer.removeEventListener('scroll', handleScroll); - }; - }, []); - - useEffect(() => { - const scrollContainer = scrollRef.current; - if (!scrollContainer || !isSubmitting || userScrolled) { - return; - } - - scrollContainer.scrollTop = scrollContainer.scrollHeight; - }, [content, isSubmitting, userScrolled]); - - return ( -
- - {currentContent} - -
- ); - }, -); - export const CopyCodeButton: React.FC<{ content: string }> = ({ content }) => { const localize = useLocalize(); const [isCopied, setIsCopied] = useState(false); diff --git a/client/src/hooks/Artifacts/useAutoScroll.ts b/client/src/hooks/Artifacts/useAutoScroll.ts deleted file mode 100644 index 1ddb9feb98..0000000000 --- a/client/src/hooks/Artifacts/useAutoScroll.ts +++ /dev/null @@ -1,47 +0,0 @@ -// hooks/useAutoScroll.ts -import { useEffect, useState } from 'react'; - -interface UseAutoScrollProps { - ref: React.RefObject; - content: string; - isSubmitting: boolean; -} - -export const useAutoScroll = ({ ref, content, isSubmitting }: UseAutoScrollProps) => { - const [userScrolled, setUserScrolled] = useState(false); - - useEffect(() => { - const scrollContainer = ref.current; - if (!scrollContainer) { - return; - } - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = scrollContainer; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; - - if (!isNearBottom) { - setUserScrolled(true); - } else { - setUserScrolled(false); - } - }; - - scrollContainer.addEventListener('scroll', handleScroll); - - return () => { - scrollContainer.removeEventListener('scroll', handleScroll); - }; - }, [ref]); - - useEffect(() => { - const scrollContainer = ref.current; - if (!scrollContainer || !isSubmitting || userScrolled) { - return; - } - - scrollContainer.scrollTop = scrollContainer.scrollHeight; - }, [content, isSubmitting, userScrolled, ref]); - - return { userScrolled }; -}; diff --git a/client/src/mobile.css b/client/src/mobile.css index 20eeb5d1da..0d31b41134 100644 --- a/client/src/mobile.css +++ b/client/src/mobile.css @@ -349,26 +349,6 @@ animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; } -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"] { - scrollbar-gutter: stable !important; - background-color: rgba(205, 205, 205, 0.66) !important; -} - -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"]::-webkit-scrollbar { - width: 12px !important; -} - -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"]::-webkit-scrollbar-thumb { - background-color: rgba(56, 56, 56) !important; - border-radius: 6px !important; - border: 2px solid transparent !important; - background-clip: padding-box !important; -} - -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"]::-webkit-scrollbar-track { - background-color: transparent !important; -} - .cm-content:focus { outline: none !important; } diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index a1caf8c07e..13f3a23b47 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -7,6 +7,7 @@ import type { const artifactFilename = { 'application/vnd.react': 'App.tsx', + 'application/vnd.ant.react': 'App.tsx', 'text/html': 'index.html', 'application/vnd.code-html': 'index.html', // mermaid and markdown types are handled separately in useArtifactProps.ts @@ -28,6 +29,7 @@ const artifactTemplate: Record< > = { 'text/html': 'static', 'application/vnd.react': 'react-ts', + 'application/vnd.ant.react': 'react-ts', 'application/vnd.mermaid': 'react-ts', 'application/vnd.code-html': 'static', 'text/markdown': 'react-ts', @@ -119,6 +121,7 @@ const dependenciesMap: Record< > = { 'application/vnd.mermaid': mermaidDependencies, 'application/vnd.react': standardDependencies, + 'application/vnd.ant.react': standardDependencies, 'text/html': standardDependencies, 'application/vnd.code-html': standardDependencies, 'text/markdown': markdownDependencies, diff --git a/package-lock.json b/package-lock.json index 6be9adfa61..e634f9f053 100644 --- a/package-lock.json +++ b/package-lock.json @@ -389,6 +389,7 @@ "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@mcp-ui/client": "^5.7.0", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", @@ -12437,6 +12438,29 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz", @@ -34283,6 +34307,40 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/monaco-editor/node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/monaco-editor/node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mongodb": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", @@ -40426,6 +40484,12 @@ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/static-browser-server": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/static-browser-server/-/static-browser-server-1.0.3.tgz", From 3a73907daac2ba7429697674d85f9f3a3f7d59a2 Mon Sep 17 00:00:00 2001 From: Carolina <143201623+CavMCarolina@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:42:23 -0300 Subject: [PATCH 002/111] =?UTF-8?q?=F0=9F=93=90=20fix:=20Replace=20JS=20Im?= =?UTF-8?q?age=20Scaling=20with=20CSS=20Viewport=20Constraints=20(#12089)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: remove scaleImage function that stretched vertical images * chore: lint * refactor: Simplify Image Component Usage Across Chat Parts - Removed height and width props from the Image component in various parts (Files, Part, ImageAttachment, LogContent) to streamline image rendering. - Introduced a constant for maximum image height in the Image component for consistent styling. - Updated related components to utilize the new simplified Image component structure, enhancing maintainability and reducing redundancy. * refactor: Simplify LogContent and Enhance Image Component Tests - Removed height and width properties from the ImageAttachment type in LogContent for cleaner code. - Updated the image rendering logic to rely solely on the filepath, improving clarity. - Enhanced the Image component tests with additional assertions for rendering behavior and accessibility. - Introduced new tests for OpenAIImageGen to validate image preloading and progress handling, ensuring robust functionality. --------- Co-authored-by: Danny Avila --- .../Chat/Messages/Content/Files.tsx | 8 - .../Chat/Messages/Content/Image.tsx | 38 +--- .../components/Chat/Messages/Content/Part.tsx | 13 +- .../Messages/Content/Parts/Attachment.tsx | 4 +- .../Messages/Content/Parts/LogContent.tsx | 33 +-- .../Parts/OpenAIImageGen/OpenAIImageGen.tsx | 99 ++------- .../Messages/Content/__tests__/Image.test.tsx | 200 ++++++++++++++++++ .../Content/__tests__/OpenAIImageGen.test.tsx | 182 ++++++++++++++++ client/src/utils/index.ts | 1 - client/src/utils/scaleImage.ts | 21 -- 10 files changed, 416 insertions(+), 183 deletions(-) create mode 100644 client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx create mode 100644 client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx delete mode 100644 client/src/utils/scaleImage.ts diff --git a/client/src/components/Chat/Messages/Content/Files.tsx b/client/src/components/Chat/Messages/Content/Files.tsx index 8997d5e822..c55148891a 100644 --- a/client/src/components/Chat/Messages/Content/Files.tsx +++ b/client/src/components/Chat/Messages/Content/Files.tsx @@ -21,15 +21,7 @@ const Files = ({ message }: { message?: TMessage }) => { ))} diff --git a/client/src/components/Chat/Messages/Content/Image.tsx b/client/src/components/Chat/Messages/Content/Image.tsx index cd72733298..502a1c8e02 100644 --- a/client/src/components/Chat/Messages/Content/Image.tsx +++ b/client/src/components/Chat/Messages/Content/Image.tsx @@ -2,26 +2,20 @@ import React, { useState, useRef, useMemo } from 'react'; import { Skeleton } from '@librechat/client'; import { LazyLoadImage } from 'react-lazy-load-image-component'; import { apiBaseUrl } from 'librechat-data-provider'; -import { cn, scaleImage } from '~/utils'; +import { cn } from '~/utils'; import DialogImage from './DialogImage'; +/** Max display height for chat images (Tailwind JIT class) */ +const IMAGE_MAX_H = 'max-h-[45vh]' as const; + const Image = ({ imagePath, altText, - height, - width, - placeholderDimensions, className, args, }: { imagePath: string; altText: string; - height: number; - width: number; - placeholderDimensions?: { - height?: string; - width?: string; - }; className?: string; args?: { prompt?: string; @@ -33,7 +27,6 @@ const Image = ({ }) => { const [isOpen, setIsOpen] = useState(false); const [isLoaded, setIsLoaded] = useState(false); - const containerRef = useRef(null); const triggerRef = useRef(null); const handleImageLoad = () => setIsLoaded(true); @@ -56,16 +49,6 @@ const Image = ({ return `${baseURL}${imagePath}`; }, [imagePath]); - const { width: scaledWidth, height: scaledHeight } = useMemo( - () => - scaleImage({ - originalWidth: Number(placeholderDimensions?.width?.split('px')[0] ?? width), - originalHeight: Number(placeholderDimensions?.height?.split('px')[0] ?? height), - containerRef, - }), - [placeholderDimensions, height, width], - ); - const downloadImage = async () => { try { const response = await fetch(absoluteImageUrl); @@ -96,7 +79,7 @@ const Image = ({ }; return ( -
+
diff --git a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx index da2a8f175e..ce6750b048 100644 --- a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx @@ -12,11 +12,7 @@ interface LogContentProps { attachments?: TAttachment[]; } -type ImageAttachment = TFile & - TAttachmentMetadata & { - height: number; - width: number; - }; +type ImageAttachment = TFile & TAttachmentMetadata; const LogContent: React.FC = ({ output = '', renderImages, attachments }) => { const localize = useLocalize(); @@ -35,12 +31,8 @@ const LogContent: React.FC = ({ output = '', renderImages, atta const nonImageAtts: TAttachment[] = []; attachments?.forEach((attachment) => { - const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata; - const isImage = - imageExtRegex.test(attachment.filename ?? '') && - width != null && - height != null && - filepath != null; + const { filepath = null } = attachment as TFile & TAttachmentMetadata; + const isImage = imageExtRegex.test(attachment.filename ?? '') && filepath != null; if (isImage) { imageAtts.push(attachment as ImageAttachment); } else { @@ -100,18 +92,13 @@ const LogContent: React.FC = ({ output = '', renderImages, atta ))}
)} - {imageAttachments?.map((attachment, index) => { - const { width, height, filepath } = attachment; - return ( - - ); - })} + {imageAttachments?.map((attachment) => ( + + ))} ); }; diff --git a/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx b/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx index aa1b07827b..e0205e4957 100644 --- a/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx @@ -1,9 +1,12 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { PixelCard } from '@librechat/client'; import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider'; import Image from '~/components/Chat/Messages/Content/Image'; import ProgressText from './ProgressText'; -import { scaleImage } from '~/utils'; +import { cn } from '~/utils'; + +const IMAGE_MAX_H = 'max-h-[45vh]' as const; +const IMAGE_FULL_H = 'h-[45vh]' as const; export default function OpenAIImageGen({ initialProgress = 0.1, @@ -28,8 +31,6 @@ export default function OpenAIImageGen({ const cancelled = (!isSubmitting && initialProgress < 1) || error === true; - let width: number | undefined; - let height: number | undefined; let quality: 'low' | 'medium' | 'high' = 'high'; // Parse args if it's a string @@ -41,61 +42,15 @@ export default function OpenAIImageGen({ parsedArgs = {}; } - try { - const argsObj = parsedArgs; - - if (argsObj && typeof argsObj.size === 'string') { - const [w, h] = argsObj.size.split('x').map((v: string) => parseInt(v, 10)); - if (!isNaN(w) && !isNaN(h)) { - width = w; - height = h; - } - } else if (argsObj && (typeof argsObj.size !== 'string' || !argsObj.size)) { - width = undefined; - height = undefined; + if (parsedArgs && typeof parsedArgs.quality === 'string') { + const q = parsedArgs.quality.toLowerCase(); + if (q === 'low' || q === 'medium' || q === 'high') { + quality = q; } - - if (argsObj && typeof argsObj.quality === 'string') { - const q = argsObj.quality.toLowerCase(); - if (q === 'low' || q === 'medium' || q === 'high') { - quality = q; - } - } - } catch (e) { - width = undefined; - height = undefined; } - // Default to 1024x1024 if width and height are still undefined after parsing args and attachment metadata const attachment = attachments?.[0]; - const { - width: imageWidth, - height: imageHeight, - filepath = null, - filename = '', - } = (attachment as TFile & TAttachmentMetadata) || {}; - - let origWidth = width ?? imageWidth; - let origHeight = height ?? imageHeight; - - if (origWidth === undefined || origHeight === undefined) { - origWidth = 1024; - origHeight = 1024; - } - - const [dimensions, setDimensions] = useState({ width: 'auto', height: 'auto' }); - const containerRef = useRef(null); - - const updateDimensions = useCallback(() => { - if (origWidth && origHeight && containerRef.current) { - const scaled = scaleImage({ - originalWidth: origWidth, - originalHeight: origHeight, - containerRef, - }); - setDimensions(scaled); - } - }, [origWidth, origHeight]); + const { filepath = null, filename = '' } = (attachment as TFile & TAttachmentMetadata) || {}; useEffect(() => { if (isSubmitting) { @@ -156,45 +111,19 @@ export default function OpenAIImageGen({ } }, [initialProgress, cancelled]); - useEffect(() => { - updateDimensions(); - - const resizeObserver = new ResizeObserver(() => { - updateDimensions(); - }); - - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [updateDimensions]); - return ( <>
-
-
- {dimensions.width !== 'auto' && progress < 1 && ( - - )} +
+
+ {progress < 1 && }
diff --git a/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx new file mode 100644 index 0000000000..3654d8e075 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Image from '../Image'; + +jest.mock('~/utils', () => ({ + cn: (...classes: unknown[]) => + classes + .flat(Infinity) + .filter((c) => typeof c === 'string' && c.length > 0) + .join(' '), +})); + +jest.mock('librechat-data-provider', () => ({ + apiBaseUrl: () => '', +})); + +jest.mock('react-lazy-load-image-component', () => ({ + LazyLoadImage: ({ + alt, + src, + className, + onLoad, + placeholder, + visibleByDefault: _visibleByDefault, + ...rest + }: { + alt: string; + src: string; + className: string; + onLoad: () => void; + placeholder: React.ReactNode; + visibleByDefault?: boolean; + [key: string]: unknown; + }) => ( +
+ {alt} +
{placeholder}
+
+ ), +})); + +jest.mock('@librechat/client', () => ({ + Skeleton: ({ className, ...props }: React.HTMLAttributes) => ( +
+ ), +})); + +jest.mock('../DialogImage', () => ({ + __esModule: true, + default: ({ isOpen, src }: { isOpen: boolean; src: string }) => + isOpen ?
: null, +})); + +describe('Image', () => { + const defaultProps = { + imagePath: '/images/test.png', + altText: 'Test image', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('rendering', () => { + it('renders with max-h-[45vh] height constraint', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img.className).toContain('max-h-[45vh]'); + }); + + it('renders with max-w-full to prevent landscape clipping', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img.className).toContain('max-w-full'); + }); + + it('renders with w-auto and h-auto for natural aspect ratio', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img.className).toContain('w-auto'); + expect(img.className).toContain('h-auto'); + }); + + it('starts with opacity-0 before image loads', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img.className).toContain('opacity-0'); + expect(img.className).not.toContain('opacity-100'); + }); + + it('transitions to opacity-100 after image loads', () => { + render(); + const img = screen.getByTestId('lazy-image'); + + fireEvent.load(img); + + expect(img.className).toContain('opacity-100'); + }); + + it('applies custom className to the button wrapper', () => { + render(); + const button = screen.getByRole('button'); + expect(button.className).toContain('mb-4'); + }); + + it('sets correct alt text', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img).toHaveAttribute('alt', 'Test image'); + }); + }); + + describe('skeleton placeholder', () => { + it('renders skeleton with non-zero dimensions', () => { + render(); + const skeleton = screen.getByTestId('skeleton'); + expect(skeleton.className).toContain('h-48'); + expect(skeleton.className).toContain('w-full'); + expect(skeleton.className).toContain('max-w-lg'); + }); + + it('renders skeleton with max-h constraint', () => { + render(); + const skeleton = screen.getByTestId('skeleton'); + expect(skeleton.className).toContain('max-h-[45vh]'); + }); + + it('has accessible loading attributes', () => { + render(); + const skeleton = screen.getByTestId('skeleton'); + expect(skeleton).toHaveAttribute('aria-label', 'Loading image'); + expect(skeleton).toHaveAttribute('aria-busy', 'true'); + }); + }); + + describe('dialog interaction', () => { + it('opens dialog on button click after image loads', () => { + render(); + + const img = screen.getByTestId('lazy-image'); + fireEvent.load(img); + + expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(screen.getByTestId('dialog-image')).toBeInTheDocument(); + }); + + it('does not render dialog before image loads', () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument(); + }); + + it('has correct accessibility attributes on button', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('aria-label', 'View Test image in dialog'); + expect(button).toHaveAttribute('aria-haspopup', 'dialog'); + }); + }); + + describe('image URL resolution', () => { + it('passes /images/ paths through with base URL', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img).toHaveAttribute('src', '/images/test.png'); + }); + + it('passes absolute http URLs through unchanged', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img).toHaveAttribute('src', 'https://example.com/photo.jpg'); + }); + + it('passes data URIs through unchanged', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img).toHaveAttribute('src', 'data:image/png;base64,abc'); + }); + + it('passes non-/images/ paths through unchanged', () => { + render(); + const img = screen.getByTestId('lazy-image'); + expect(img).toHaveAttribute('src', '/other/path.png'); + }); + }); +}); diff --git a/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx new file mode 100644 index 0000000000..886b1b6294 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx @@ -0,0 +1,182 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import OpenAIImageGen from '../Parts/OpenAIImageGen/OpenAIImageGen'; + +jest.mock('~/utils', () => ({ + cn: (...classes: unknown[]) => + classes + .flat(Infinity) + .filter((c) => typeof c === 'string' && c.length > 0) + .join(' '), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, +})); + +jest.mock('~/components/Chat/Messages/Content/Image', () => ({ + __esModule: true, + default: ({ + altText, + imagePath, + className, + }: { + altText: string; + imagePath: string; + className?: string; + }) => ( +
+ ), +})); + +jest.mock('@librechat/client', () => ({ + PixelCard: ({ progress }: { progress: number }) => ( +
+ ), +})); + +jest.mock('../Parts/OpenAIImageGen/ProgressText', () => ({ + __esModule: true, + default: ({ progress, error }: { progress: number; error: boolean }) => ( +
+ ), +})); + +describe('OpenAIImageGen', () => { + const defaultProps = { + initialProgress: 0.1, + isSubmitting: true, + toolName: 'image_gen_oai', + args: '{"prompt":"a cat","quality":"high","size":"1024x1024"}', + output: null as string | null, + attachments: undefined, + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('image preloading', () => { + it('keeps Image mounted during generation (progress < 1)', () => { + render(); + expect(screen.getByTestId('image-component')).toBeInTheDocument(); + }); + + it('hides Image with invisible absolute while progress < 1', () => { + render(); + const image = screen.getByTestId('image-component'); + expect(image.className).toContain('invisible'); + expect(image.className).toContain('absolute'); + }); + + it('shows Image without hiding classes when progress >= 1', () => { + render( + , + ); + const image = screen.getByTestId('image-component'); + expect(image.className).not.toContain('invisible'); + expect(image.className).not.toContain('absolute'); + }); + }); + + describe('PixelCard visibility', () => { + it('shows PixelCard when progress < 1', () => { + render(); + expect(screen.getByTestId('pixel-card')).toBeInTheDocument(); + }); + + it('hides PixelCard when progress >= 1', () => { + render(); + expect(screen.queryByTestId('pixel-card')).not.toBeInTheDocument(); + }); + }); + + describe('layout classes', () => { + it('applies max-h-[45vh] to the outer container', () => { + const { container } = render(); + const outerDiv = container.querySelector('[class*="max-h-"]'); + expect(outerDiv?.className).toContain('max-h-[45vh]'); + }); + + it('applies h-[45vh] w-full to inner container during loading', () => { + const { container } = render(); + const innerDiv = container.querySelector('[class*="h-[45vh]"]'); + expect(innerDiv).not.toBeNull(); + expect(innerDiv?.className).toContain('w-full'); + }); + + it('applies w-auto to inner container when complete', () => { + const { container } = render( + , + ); + const overflowDiv = container.querySelector('[class*="overflow-hidden"]'); + expect(overflowDiv?.className).toContain('w-auto'); + }); + }); + + describe('args parsing', () => { + it('parses quality from args', () => { + render(); + expect(screen.getByTestId('progress-text')).toBeInTheDocument(); + }); + + it('handles invalid JSON args gracefully', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + render(); + expect(screen.getByTestId('image-component')).toBeInTheDocument(); + consoleSpy.mockRestore(); + }); + + it('handles object args', () => { + render( + , + ); + expect(screen.getByTestId('image-component')).toBeInTheDocument(); + }); + }); + + describe('cancellation', () => { + it('shows error state when output contains error', () => { + render( + , + ); + const progressText = screen.getByTestId('progress-text'); + expect(progressText).toHaveAttribute('data-error', 'true'); + }); + + it('shows cancelled state when not submitting and incomplete', () => { + render(); + const progressText = screen.getByTestId('progress-text'); + expect(progressText).toHaveAttribute('data-error', 'true'); + }); + }); +}); diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts index 8946951ed8..2a7dfc4a88 100644 --- a/client/src/utils/index.ts +++ b/client/src/utils/index.ts @@ -29,7 +29,6 @@ export * from './share'; export * from './timestamps'; export { default as cn } from './cn'; export { default as logger } from './logger'; -export { default as scaleImage } from './scaleImage'; export { default as getLoginError } from './getLoginError'; export { default as cleanupPreset } from './cleanupPreset'; export { default as buildDefaultConvo } from './buildDefaultConvo'; diff --git a/client/src/utils/scaleImage.ts b/client/src/utils/scaleImage.ts deleted file mode 100644 index 11e051fbd9..0000000000 --- a/client/src/utils/scaleImage.ts +++ /dev/null @@ -1,21 +0,0 @@ -export default function scaleImage({ - originalWidth, - originalHeight, - containerRef, -}: { - originalWidth?: number; - originalHeight?: number; - containerRef: React.RefObject; -}) { - const containerWidth = containerRef.current?.offsetWidth ?? 0; - - if (containerWidth === 0 || originalWidth == null || originalHeight == null) { - return { width: 'auto', height: 'auto' }; - } - - const aspectRatio = originalWidth / originalHeight; - const scaledWidth = Math.min(containerWidth, originalWidth); - const scaledHeight = scaledWidth / aspectRatio; - - return { width: `${scaledWidth}px`, height: `${scaledHeight}px` }; -} From cc3d62c64038b108381c6be6d0796299323babce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Airam=20Hern=C3=A1ndez=20Hern=C3=A1ndez?= <100208966+Airamhh@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:55:05 +0000 Subject: [PATCH 003/111] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20Add=20Pe?= =?UTF-8?q?rmission=20Guard=20for=20Temporary=20Chat=20Visibility=20(#1210?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add useHasAccess hook for TEMPORARY_CHAT permission type - Conditionally render TemporaryChat component based on user permissions - Ensures feature respects role-based access control Co-authored-by: Airam Hernández Hernández --- client/src/components/Chat/Header.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/components/Chat/Header.tsx b/client/src/components/Chat/Header.tsx index 611e96fdcc..9e44e804c9 100644 --- a/client/src/components/Chat/Header.tsx +++ b/client/src/components/Chat/Header.tsx @@ -35,6 +35,11 @@ function Header() { permission: Permissions.USE, }); + const hasAccessToTemporaryChat = useHasAccess({ + permissionType: PermissionTypes.TEMPORARY_CHAT, + permission: Permissions.USE, + }); + const isSmallScreen = useMediaQuery('(max-width: 768px)'); return ( @@ -73,7 +78,7 @@ function Header() { - + {hasAccessToTemporaryChat === true && } )}
@@ -85,7 +90,7 @@ function Header() { - + {hasAccessToTemporaryChat === true && }
)}
From 6d0938be647d2053154153d19973ffedf13a6322 Mon Sep 17 00:00:00 2001 From: Lionel Ringenbach Date: Fri, 6 Mar 2026 16:05:56 -0800 Subject: [PATCH 004/111] =?UTF-8?q?=F0=9F=94=92=20refactor:=20Set=20`ALLOW?= =?UTF-8?q?=5FSHARED=5FLINKS=5FPUBLIC`=20to=20`false`=20by=20Default=20(#1?= =?UTF-8?q?2100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: default ALLOW_SHARED_LINKS_PUBLIC to false for security Shared links were publicly accessible by default when ALLOW_SHARED_LINKS_PUBLIC was not explicitly set, which could lead to unintentional data exposure. Users may assume their authentication settings protect shared links when they do not. This changes the default behavior so shared links require JWT authentication unless ALLOW_SHARED_LINKS_PUBLIC is explicitly set to true. * Document ALLOW_SHARED_LINKS_PUBLIC in .env.example Add comment explaining ALLOW_SHARED_LINKS_PUBLIC setting. --------- Co-authored-by: Claude Co-authored-by: Danny Avila --- .env.example | 3 ++- api/server/routes/config.js | 4 +--- api/server/routes/share.js | 4 +--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 06d509a3ae..b851749baf 100644 --- a/.env.example +++ b/.env.example @@ -677,7 +677,8 @@ AZURE_CONTAINER_NAME=files #========================# ALLOW_SHARED_LINKS=true -ALLOW_SHARED_LINKS_PUBLIC=true +# Allows unauthenticated access to shared links. Defaults to false (auth required) if not set. +ALLOW_SHARED_LINKS_PUBLIC=false #==============================# # Static File Cache Control # diff --git a/api/server/routes/config.js b/api/server/routes/config.js index a2dc5b79d2..0adc9272bb 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -16,9 +16,7 @@ const sharedLinksEnabled = process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS); const publicSharedLinksEnabled = - sharedLinksEnabled && - (process.env.ALLOW_SHARED_LINKS_PUBLIC === undefined || - isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC)); + sharedLinksEnabled && isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC); const sharePointFilePickerEnabled = isEnabled(process.env.ENABLE_SHAREPOINT_FILEPICKER); const openidReuseTokens = isEnabled(process.env.OPENID_REUSE_TOKENS); diff --git a/api/server/routes/share.js b/api/server/routes/share.js index 6400b8b637..296644afde 100644 --- a/api/server/routes/share.js +++ b/api/server/routes/share.js @@ -19,9 +19,7 @@ const allowSharedLinks = process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS); if (allowSharedLinks) { - const allowSharedLinksPublic = - process.env.ALLOW_SHARED_LINKS_PUBLIC === undefined || - isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC); + const allowSharedLinksPublic = isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC); router.get( '/:shareId', allowSharedLinksPublic ? (req, res, next) => next() : requireJwtAuth, From b93d60c416603f64e7e2c3cf16ef0e35aba96b23 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 6 Mar 2026 19:09:52 -0500 Subject: [PATCH 005/111] =?UTF-8?q?=F0=9F=8E=9E=EF=B8=8F=20refactor:=20Ima?= =?UTF-8?q?ge=20Rendering=20with=20Preview=20Caching=20and=20Layout=20Rese?= =?UTF-8?q?rvation=20(#12114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Update Image Component to Remove Lazy Loading and Enhance Rendering - Removed the react-lazy-load-image-component dependency from the Image component, simplifying the image loading process. - Updated the Image component to use a standard tag with async decoding for improved performance and user experience. - Adjusted related tests to reflect changes in image rendering behavior and ensure proper functionality without lazy loading. * refactor: Enhance Image Handling and Caching Across Components - Introduced a new previewCache utility for managing local blob preview URLs, improving image loading efficiency. - Updated the Image component and related parts (FileRow, Files, Part, ImageAttachment, LogContent) to utilize cached previews, enhancing rendering performance and user experience. - Added width and height properties to the Image component for better layout management and consistency across different usages. - Improved file handling logic in useFileHandling to cache previews during file uploads, ensuring quick access to image data. - Enhanced overall code clarity and maintainability by streamlining image rendering logic and reducing redundancy. * refactor: Enhance OpenAIImageGen Component with Image Dimensions - Added width and height properties to the OpenAIImageGen component for improved image rendering and layout management. - Updated the Image component usage within OpenAIImageGen to utilize the new dimensions, enhancing visual consistency and performance. - Improved code clarity by destructuring additional properties from the attachment object, streamlining the component's logic. * refactor: Implement Image Size Caching in DialogImage Component - Introduced an imageSizeCache to store and retrieve image sizes, enhancing performance by reducing redundant fetch requests. - Updated the getImageSize function to first check the cache before making network requests, improving efficiency in image handling. - Added decoding attribute to the image element for optimized rendering behavior. * refactor: Enhance UserAvatar Component with Avatar Caching and Error Handling - Introduced avatar caching logic to optimize avatar resolution based on user ID and avatar source, improving performance and reducing redundant image loads. - Implemented error handling for failed image loads, allowing for fallback to a default avatar when necessary. - Updated UserAvatar props to streamline the interface by removing the user object and directly accepting avatar-related properties. - Enhanced overall code clarity and maintainability by refactoring the component structure and logic. * fix: Layout Shift in Message and Placeholder Components for Consistent Height Management - Adjusted the height of the PlaceholderRow and related message components to ensure consistent rendering with a minimum height of 31px. - Updated the MessageParts and ContentRender components to utilize a minimum height for better layout stability. - Enhanced overall code clarity by standardizing the structure of message-related components. * tests: Update FileRow Component to Prefer Cached Previews for Image Rendering - Modified the image URL selection logic in the FileRow component to prioritize cached previews over file paths when uploads are complete, enhancing rendering performance and user experience. - Updated related tests to reflect changes in image URL handling, ensuring accurate assertions for both preview and file path scenarios. - Introduced a fallback mechanism to use file paths when no preview exists, improving robustness in file handling. * fix: Image cache lifecycle and dialog decoding - Add deletePreview/clearPreviewCache to previewCache.ts for blob URL cleanup - Wire deletePreview into useFileDeletion to revoke blobs on file delete - Move dimensionCache.set into useMemo to avoid side effects during render - Extract IMAGE_MAX_W_PX constant (512) to document coupling with max-w-lg - Export _resetImageCaches for test isolation - Change DialogImage decoding from "sync" to "async" to avoid blocking main thread * fix: Avatar cache invalidation and cleanup - Include avatarSrc in cache invalidation to prevent stale avatars - Remove unused username parameter from resolveAvatar - Skip caching when userId is empty to prevent cache key collisions * test: Fix test isolation and type safety - Reset module-level dimensionCache/paintedUrls in beforeEach via _resetImageCaches - Replace any[] with typed mock signature in cn mock for both test files * chore: Code quality improvements from review - Use barrel imports for previewCache in Files.tsx and Part.tsx - Single Map.get with truthy check instead of has+get in useEventHandlers - Add JSDoc comments explaining EmptyText margin removal and PlaceholderRow height - Fix FileRow toast showing "Deleting file" when file isn't actually deleted (progress < 1) * fix: Address remaining review findings (R1-R3) - Add deletePreview calls to deleteFiles batch path to prevent blob URL leaks - Change useFileDeletion import from deep path to barrel (~/utils) - Change useMemo to useEffect for dimensionCache.set (side effect, not derived value) * fix: Address audit comments 2, 5, and 7 - Fix files preservation to distinguish null (missing) from [] (empty) in finalHandler - Add auto-revoke on overwrite in cachePreview to prevent leaked blobs - Add removePreviewEntry for key transfer without revoke - Clean up stale temp_file_id cache entry after promotion to permanent file_id --- client/package.json | 3 +- .../components/Chat/Input/Files/FileRow.tsx | 14 +- .../Input/Files/__tests__/FileRow.spec.tsx | 36 +++- .../Chat/Messages/Content/DialogImage.tsx | 15 +- .../Chat/Messages/Content/Files.tsx | 20 +- .../Chat/Messages/Content/Image.tsx | 90 +++++---- .../components/Chat/Messages/Content/Part.tsx | 9 +- .../Messages/Content/Parts/Attachment.tsx | 4 +- .../Chat/Messages/Content/Parts/EmptyText.tsx | 3 +- .../Messages/Content/Parts/LogContent.tsx | 2 + .../Parts/OpenAIImageGen/OpenAIImageGen.tsx | 11 +- .../Messages/Content/__tests__/Image.test.tsx | 177 ++++++++---------- .../Content/__tests__/OpenAIImageGen.test.tsx | 4 +- .../components/Chat/Messages/MessageParts.tsx | 4 +- .../Chat/Messages/ui/MessageRender.tsx | 2 +- .../Chat/Messages/ui/PlaceholderRow.tsx | 3 +- client/src/components/Endpoints/Icon.tsx | 131 ++++++++----- .../src/components/Messages/ContentRender.tsx | 2 +- client/src/components/Share/Message.tsx | 2 +- .../Files/__tests__/useFileHandling.test.ts | 8 +- client/src/hooks/Files/useFileDeletion.ts | 11 ++ client/src/hooks/Files/useFileHandling.ts | 10 +- client/src/hooks/SSE/useEventHandlers.ts | 17 ++ client/src/utils/index.ts | 1 + client/src/utils/previewCache.ts | 35 ++++ package-lock.json | 23 +-- 26 files changed, 390 insertions(+), 247 deletions(-) create mode 100644 client/src/utils/previewCache.ts diff --git a/client/package.json b/client/package.json index 31e75c4702..c588ccc6d9 100644 --- a/client/package.json +++ b/client/package.json @@ -94,7 +94,6 @@ "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", "react-i18next": "^15.4.0", - "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^3.0.6", "react-router-dom": "^6.30.3", @@ -147,9 +146,9 @@ "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^30.2.0", - "monaco-editor": "^0.55.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", + "monaco-editor": "^0.55.0", "postcss": "^8.4.31", "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", diff --git a/client/src/components/Chat/Input/Files/FileRow.tsx b/client/src/components/Chat/Input/Files/FileRow.tsx index ba8e73a8a2..bf04b16ade 100644 --- a/client/src/components/Chat/Input/Files/FileRow.tsx +++ b/client/src/components/Chat/Input/Files/FileRow.tsx @@ -3,10 +3,10 @@ import { useToastContext } from '@librechat/client'; import { EToolResources } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import { useDeleteFilesMutation } from '~/data-provider'; +import { logger, getCachedPreview } from '~/utils'; import { useFileDeletion } from '~/hooks/Files'; import FileContainer from './FileContainer'; import { useLocalize } from '~/hooks'; -import { logger } from '~/utils'; import Image from './Image'; export default function FileRow({ @@ -112,13 +112,15 @@ export default function FileRow({ ) .uniqueFiles.map((file: ExtendedFile, index: number) => { const handleDelete = () => { - showToast({ - message: localize('com_ui_deleting_file'), - status: 'info', - }); if (abortUpload && file.progress < 1) { abortUpload(); } + if (file.progress >= 1) { + showToast({ + message: localize('com_ui_deleting_file'), + status: 'info', + }); + } deleteFile({ file, setFiles }); }; const isImage = file.type?.startsWith('image') ?? false; @@ -134,7 +136,7 @@ export default function FileRow({ > {isImage ? ( ({ logger: { log: jest.fn(), }, + getCachedPreview: jest.fn(() => undefined), })); jest.mock('../Image', () => { @@ -95,7 +96,7 @@ describe('FileRow', () => { }; describe('Image URL Selection Logic', () => { - it('should use filepath instead of preview when progress is 1 (upload complete)', () => { + it('should prefer cached preview over filepath when upload is complete', () => { const file = createMockFile({ file_id: 'uploaded-file', preview: 'blob:http://localhost:3080/temp-preview', @@ -109,8 +110,7 @@ describe('FileRow', () => { renderFileRow(filesMap); const imageUrl = screen.getByTestId('image-url').textContent; - expect(imageUrl).toBe('/images/user123/uploaded-file__image.png'); - expect(imageUrl).not.toContain('blob:'); + expect(imageUrl).toBe('blob:http://localhost:3080/temp-preview'); }); it('should use preview when progress is less than 1 (uploading)', () => { @@ -147,7 +147,7 @@ describe('FileRow', () => { expect(imageUrl).toBe('/images/user123/file-without-preview__image.png'); }); - it('should use filepath when both preview and filepath exist and progress is exactly 1', () => { + it('should prefer preview over filepath when both exist and progress is 1', () => { const file = createMockFile({ file_id: 'complete-file', preview: 'blob:http://localhost:3080/old-blob', @@ -161,7 +161,7 @@ describe('FileRow', () => { renderFileRow(filesMap); const imageUrl = screen.getByTestId('image-url').textContent; - expect(imageUrl).toBe('/images/user123/complete-file__image.png'); + expect(imageUrl).toBe('blob:http://localhost:3080/old-blob'); }); }); @@ -284,7 +284,7 @@ describe('FileRow', () => { const urls = screen.getAllByTestId('image-url').map((el) => el.textContent); expect(urls).toContain('blob:http://localhost:3080/preview-1'); - expect(urls).toContain('/images/user123/file-2__image.png'); + expect(urls).toContain('blob:http://localhost:3080/preview-2'); }); it('should deduplicate files with the same file_id', () => { @@ -321,10 +321,10 @@ describe('FileRow', () => { }); }); - describe('Regression: Blob URL Bug Fix', () => { - it('should NOT use revoked blob URL after upload completes', () => { + describe('Preview Cache Integration', () => { + it('should prefer preview blob URL over filepath for zero-flicker rendering', () => { const file = createMockFile({ - file_id: 'regression-test', + file_id: 'cache-test', preview: 'blob:http://localhost:3080/d25f730c-152d-41f7-8d79-c9fa448f606b', filepath: '/images/68c98b26901ebe2d87c193a2/c0fe1b93-ba3d-456c-80be-9a492bfd9ed0__image.png', @@ -337,8 +337,24 @@ describe('FileRow', () => { renderFileRow(filesMap); const imageUrl = screen.getByTestId('image-url').textContent; + expect(imageUrl).toBe('blob:http://localhost:3080/d25f730c-152d-41f7-8d79-c9fa448f606b'); + }); - expect(imageUrl).not.toContain('blob:'); + it('should fall back to filepath when no preview exists', () => { + const file = createMockFile({ + file_id: 'no-preview', + preview: undefined, + filepath: + '/images/68c98b26901ebe2d87c193a2/c0fe1b93-ba3d-456c-80be-9a492bfd9ed0__image.png', + progress: 1, + }); + + const filesMap = new Map(); + filesMap.set(file.file_id, file); + + renderFileRow(filesMap); + + const imageUrl = screen.getByTestId('image-url').textContent; expect(imageUrl).toBe( '/images/68c98b26901ebe2d87c193a2/c0fe1b93-ba3d-456c-80be-9a492bfd9ed0__image.png', ); diff --git a/client/src/components/Chat/Messages/Content/DialogImage.tsx b/client/src/components/Chat/Messages/Content/DialogImage.tsx index cb496de646..b9cbe64555 100644 --- a/client/src/components/Chat/Messages/Content/DialogImage.tsx +++ b/client/src/components/Chat/Messages/Content/DialogImage.tsx @@ -4,6 +4,8 @@ import { Button, TooltipAnchor } from '@librechat/client'; import { X, ArrowDownToLine, PanelLeftOpen, PanelLeftClose, RotateCcw } from 'lucide-react'; import { useLocalize } from '~/hooks'; +const imageSizeCache = new Map(); + const getQualityStyles = (quality: string): string => { if (quality === 'high') { return 'bg-green-100 text-green-800'; @@ -50,18 +52,26 @@ export default function DialogImage({ const closeButtonRef = useRef(null); const getImageSize = useCallback(async (url: string) => { + const cached = imageSizeCache.get(url); + if (cached) { + return cached; + } try { const response = await fetch(url, { method: 'HEAD' }); const contentLength = response.headers.get('Content-Length'); if (contentLength) { const bytes = parseInt(contentLength, 10); - return formatFileSize(bytes); + const result = formatFileSize(bytes); + imageSizeCache.set(url, result); + return result; } const fullResponse = await fetch(url); const blob = await fullResponse.blob(); - return formatFileSize(blob.size); + const result = formatFileSize(blob.size); + imageSizeCache.set(url, result); + return result; } catch (error) { console.error('Error getting image size:', error); return null; @@ -355,6 +365,7 @@ export default function DialogImage({ ref={imageRef} src={src} alt="Image" + decoding="async" className="block max-h-[85vh] object-contain" style={{ maxWidth: getImageMaxWidth(), diff --git a/client/src/components/Chat/Messages/Content/Files.tsx b/client/src/components/Chat/Messages/Content/Files.tsx index c55148891a..504e48e883 100644 --- a/client/src/components/Chat/Messages/Content/Files.tsx +++ b/client/src/components/Chat/Messages/Content/Files.tsx @@ -1,6 +1,7 @@ import { useMemo, memo } from 'react'; import type { TFile, TMessage } from 'librechat-data-provider'; import FileContainer from '~/components/Chat/Input/Files/FileContainer'; +import { getCachedPreview } from '~/utils'; import Image from './Image'; const Files = ({ message }: { message?: TMessage }) => { @@ -17,13 +18,18 @@ const Files = ({ message }: { message?: TMessage }) => { {otherFiles.length > 0 && otherFiles.map((file) => )} {imageFiles.length > 0 && - imageFiles.map((file) => ( - - ))} + imageFiles.map((file) => { + const cached = file.file_id ? getCachedPreview(file.file_id) : undefined; + return ( + + ); + })} ); }; diff --git a/client/src/components/Chat/Messages/Content/Image.tsx b/client/src/components/Chat/Messages/Content/Image.tsx index 502a1c8e02..7e3e12e65b 100644 --- a/client/src/components/Chat/Messages/Content/Image.tsx +++ b/client/src/components/Chat/Messages/Content/Image.tsx @@ -1,18 +1,36 @@ -import React, { useState, useRef, useMemo } from 'react'; +import React, { useState, useRef, useMemo, useEffect } from 'react'; import { Skeleton } from '@librechat/client'; -import { LazyLoadImage } from 'react-lazy-load-image-component'; import { apiBaseUrl } from 'librechat-data-provider'; -import { cn } from '~/utils'; import DialogImage from './DialogImage'; +import { cn } from '~/utils'; /** Max display height for chat images (Tailwind JIT class) */ -const IMAGE_MAX_H = 'max-h-[45vh]' as const; +export const IMAGE_MAX_H = 'max-h-[45vh]' as const; +/** Matches the `max-w-lg` Tailwind class on the wrapper button (32rem = 512px at 16px base) */ +const IMAGE_MAX_W_PX = 512; + +/** Caches image dimensions by src so remounts can reserve space */ +const dimensionCache = new Map(); +/** Tracks URLs that have been fully painted — skip skeleton on remount */ +const paintedUrls = new Set(); + +/** Test-only: resets module-level caches */ +export function _resetImageCaches(): void { + dimensionCache.clear(); + paintedUrls.clear(); +} + +function computeHeightStyle(w: number, h: number): React.CSSProperties { + return { height: `min(45vh, ${(h / w) * 100}vw, ${(h / w) * IMAGE_MAX_W_PX}px)` }; +} const Image = ({ imagePath, altText, className, args, + width, + height, }: { imagePath: string; altText: string; @@ -24,18 +42,15 @@ const Image = ({ style?: string; [key: string]: unknown; }; + width?: number; + height?: number; }) => { const [isOpen, setIsOpen] = useState(false); - const [isLoaded, setIsLoaded] = useState(false); const triggerRef = useRef(null); - const handleImageLoad = () => setIsLoaded(true); - - // Fix image path to include base path for subdirectory deployments const absoluteImageUrl = useMemo(() => { if (!imagePath) return imagePath; - // If it's already an absolute URL or doesn't start with /images/, return as is if ( imagePath.startsWith('http') || imagePath.startsWith('data:') || @@ -44,7 +59,6 @@ const Image = ({ return imagePath; } - // Get the base URL and prepend it to the image path const baseURL = apiBaseUrl(); return `${baseURL}${imagePath}`; }, [imagePath]); @@ -78,6 +92,17 @@ const Image = ({ } }; + useEffect(() => { + if (width && height && absoluteImageUrl) { + dimensionCache.set(absoluteImageUrl, { width, height }); + } + }, [absoluteImageUrl, width, height]); + + const dims = width && height ? { width, height } : dimensionCache.get(absoluteImageUrl); + const hasDimensions = !!(dims?.width && dims?.height); + const heightStyle = hasDimensions ? computeHeightStyle(dims.width, dims.height) : undefined; + const showSkeleton = hasDimensions && !paintedUrls.has(absoluteImageUrl); + return (
- {isLoaded && ( - - )} +
); }; diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index 55946d64d5..7bce7ac11d 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -11,6 +11,7 @@ import type { TMessageContentParts, TAttachment } from 'librechat-data-provider' import { OpenAIImageGen, EmptyText, Reasoning, ExecuteCode, AgentUpdate, Text } from './Parts'; import { ErrorMessage } from './MessageContent'; import RetrievalCall from './RetrievalCall'; +import { getCachedPreview } from '~/utils'; import AgentHandoff from './AgentHandoff'; import CodeAnalyze from './CodeAnalyze'; import Container from './Container'; @@ -222,8 +223,14 @@ const Part = memo(function Part({ } } else if (part.type === ContentTypes.IMAGE_FILE) { const imageFile = part[ContentTypes.IMAGE_FILE]; + const cached = imageFile.file_id ? getCachedPreview(imageFile.file_id) : undefined; return ( - + ); } diff --git a/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx b/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx index 132bac51ea..31e30772dc 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx @@ -52,7 +52,7 @@ const FileAttachment = memo(({ attachment }: { attachment: Partial const ImageAttachment = memo(({ attachment }: { attachment: TAttachment }) => { const [isLoaded, setIsLoaded] = useState(false); - const { filepath = null } = attachment as TFile & TAttachmentMetadata; + const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata; useEffect(() => { setIsLoaded(false); @@ -76,6 +76,8 @@ const ImageAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
diff --git a/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx b/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx index 1b514164df..409461a058 100644 --- a/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx @@ -1,8 +1,9 @@ import { memo } from 'react'; +/** Streaming cursor placeholder — no bottom margin to match Container's structure and prevent CLS */ const EmptyTextPart = memo(() => { return ( -
+

diff --git a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx index ce6750b048..a675ff06d8 100644 --- a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx @@ -94,6 +94,8 @@ const LogContent: React.FC = ({ output = '', renderImages, atta )} {imageAttachments?.map((attachment) => ( { if (isSubmitting) { @@ -120,9 +125,11 @@ export default function OpenAIImageGen({

{progress < 1 && }
diff --git a/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx index 3654d8e075..e7e0b99f1e 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; -import Image from '../Image'; +import Image, { _resetImageCaches } from '../Image'; jest.mock('~/utils', () => ({ - cn: (...classes: unknown[]) => + cn: (...classes: (string | boolean | undefined | null)[]) => classes .flat(Infinity) - .filter((c) => typeof c === 'string' && c.length > 0) + .filter((c): c is string => typeof c === 'string' && c.length > 0) .join(' '), })); @@ -14,38 +14,6 @@ jest.mock('librechat-data-provider', () => ({ apiBaseUrl: () => '', })); -jest.mock('react-lazy-load-image-component', () => ({ - LazyLoadImage: ({ - alt, - src, - className, - onLoad, - placeholder, - visibleByDefault: _visibleByDefault, - ...rest - }: { - alt: string; - src: string; - className: string; - onLoad: () => void; - placeholder: React.ReactNode; - visibleByDefault?: boolean; - [key: string]: unknown; - }) => ( -
- {alt} -
{placeholder}
-
- ), -})); - jest.mock('@librechat/client', () => ({ Skeleton: ({ className, ...props }: React.HTMLAttributes) => (
@@ -65,45 +33,84 @@ describe('Image', () => { }; beforeEach(() => { + _resetImageCaches(); jest.clearAllMocks(); }); - describe('rendering', () => { + describe('rendering without dimensions', () => { it('renders with max-h-[45vh] height constraint', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img.className).toContain('max-h-[45vh]'); }); it('renders with max-w-full to prevent landscape clipping', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img.className).toContain('max-w-full'); }); it('renders with w-auto and h-auto for natural aspect ratio', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img.className).toContain('w-auto'); expect(img.className).toContain('h-auto'); }); - it('starts with opacity-0 before image loads', () => { + it('does not show skeleton without dimensions', () => { render(); - const img = screen.getByTestId('lazy-image'); - expect(img.className).toContain('opacity-0'); - expect(img.className).not.toContain('opacity-100'); + expect(screen.queryByTestId('skeleton')).not.toBeInTheDocument(); }); - it('transitions to opacity-100 after image loads', () => { + it('does not apply heightStyle without dimensions', () => { render(); - const img = screen.getByTestId('lazy-image'); + const button = screen.getByRole('button'); + expect(button.style.height).toBeFalsy(); + }); + }); + + describe('rendering with dimensions', () => { + it('shows skeleton behind image', () => { + render(); + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + it('applies computed heightStyle to button', () => { + render(); + const button = screen.getByRole('button'); + expect(button.style.height).toBeTruthy(); + expect(button.style.height).toContain('min(45vh'); + }); + + it('uses size-full object-contain on image when dimensions provided', () => { + render(); + const img = screen.getByRole('img'); + expect(img.className).toContain('size-full'); + expect(img.className).toContain('object-contain'); + }); + + it('skeleton is absolute inset-0', () => { + render(); + const skeleton = screen.getByTestId('skeleton'); + expect(skeleton.className).toContain('absolute'); + expect(skeleton.className).toContain('inset-0'); + }); + + it('marks URL as painted on load and skips skeleton on rerender', () => { + const { rerender } = render(); + const img = screen.getByRole('img'); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); fireEvent.load(img); - expect(img.className).toContain('opacity-100'); + // Rerender same component — skeleton should not show (URL painted) + rerender(); + expect(screen.queryByTestId('skeleton')).not.toBeInTheDocument(); }); + }); + describe('common behavior', () => { it('applies custom className to the button wrapper', () => { render(); const button = screen.getByRole('button'); @@ -112,57 +119,9 @@ describe('Image', () => { it('sets correct alt text', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img).toHaveAttribute('alt', 'Test image'); }); - }); - - describe('skeleton placeholder', () => { - it('renders skeleton with non-zero dimensions', () => { - render(); - const skeleton = screen.getByTestId('skeleton'); - expect(skeleton.className).toContain('h-48'); - expect(skeleton.className).toContain('w-full'); - expect(skeleton.className).toContain('max-w-lg'); - }); - - it('renders skeleton with max-h constraint', () => { - render(); - const skeleton = screen.getByTestId('skeleton'); - expect(skeleton.className).toContain('max-h-[45vh]'); - }); - - it('has accessible loading attributes', () => { - render(); - const skeleton = screen.getByTestId('skeleton'); - expect(skeleton).toHaveAttribute('aria-label', 'Loading image'); - expect(skeleton).toHaveAttribute('aria-busy', 'true'); - }); - }); - - describe('dialog interaction', () => { - it('opens dialog on button click after image loads', () => { - render(); - - const img = screen.getByTestId('lazy-image'); - fireEvent.load(img); - - expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument(); - - const button = screen.getByRole('button'); - fireEvent.click(button); - - expect(screen.getByTestId('dialog-image')).toBeInTheDocument(); - }); - - it('does not render dialog before image loads', () => { - render(); - - const button = screen.getByRole('button'); - fireEvent.click(button); - - expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument(); - }); it('has correct accessibility attributes on button', () => { render(); @@ -172,28 +131,48 @@ describe('Image', () => { }); }); + describe('dialog interaction', () => { + it('opens dialog on button click', () => { + render(); + expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(screen.getByTestId('dialog-image')).toBeInTheDocument(); + }); + + it('dialog is always mounted (not gated by load state)', () => { + render(); + // DialogImage mock returns null when isOpen=false, but the component is in the tree + // Clicking should immediately show it + fireEvent.click(screen.getByRole('button')); + expect(screen.getByTestId('dialog-image')).toBeInTheDocument(); + }); + }); + describe('image URL resolution', () => { it('passes /images/ paths through with base URL', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img).toHaveAttribute('src', '/images/test.png'); }); it('passes absolute http URLs through unchanged', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img).toHaveAttribute('src', 'https://example.com/photo.jpg'); }); it('passes data URIs through unchanged', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img).toHaveAttribute('src', 'data:image/png;base64,abc'); }); it('passes non-/images/ paths through unchanged', () => { render(); - const img = screen.getByTestId('lazy-image'); + const img = screen.getByRole('img'); expect(img).toHaveAttribute('src', '/other/path.png'); }); }); diff --git a/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx index 886b1b6294..ef8ac2807a 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx @@ -3,10 +3,10 @@ import { render, screen } from '@testing-library/react'; import OpenAIImageGen from '../Parts/OpenAIImageGen/OpenAIImageGen'; jest.mock('~/utils', () => ({ - cn: (...classes: unknown[]) => + cn: (...classes: (string | boolean | undefined | null)[]) => classes .flat(Infinity) - .filter((c) => typeof c === 'string' && c.length > 0) + .filter((c): c is string => typeof c === 'string' && c.length > 0) .join(' '), })); diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx index 88162f3287..7aa73a54e6 100644 --- a/client/src/components/Chat/Messages/MessageParts.tsx +++ b/client/src/components/Chat/Messages/MessageParts.tsx @@ -129,7 +129,7 @@ export default function Message(props: TMessageProps) { )}
-
+
{isLast && isSubmitting ? ( -
+
) : ( -
+
; + return
; }); PlaceholderRow.displayName = 'PlaceholderRow'; diff --git a/client/src/components/Endpoints/Icon.tsx b/client/src/components/Endpoints/Icon.tsx index 3256145bfb..fae0f286d3 100644 --- a/client/src/components/Endpoints/Icon.tsx +++ b/client/src/components/Endpoints/Icon.tsx @@ -1,64 +1,102 @@ -import React, { memo, useState } from 'react'; +import React, { memo } from 'react'; import { UserIcon, useAvatar } from '@librechat/client'; -import type { TUser } from 'librechat-data-provider'; import type { IconProps } from '~/common'; import MessageEndpointIcon from './MessageEndpointIcon'; import { useAuthContext } from '~/hooks/AuthContext'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; +type ResolvedAvatar = { type: 'image'; src: string } | { type: 'fallback' }; + +/** + * Caches the resolved avatar decision per user ID. + * Invalidated when `user.avatar` changes (e.g., settings upload). + * Tracks failed image URLs so they fall back to SVG permanently for the session. + */ +const avatarCache = new Map< + string, + { avatar: string; avatarSrc: string; resolved: ResolvedAvatar } +>(); +const failedUrls = new Set(); + +function resolveAvatar(userId: string, userAvatar: string, avatarSrc: string): ResolvedAvatar { + if (!userId) { + const imgSrc = userAvatar || avatarSrc; + return imgSrc && !failedUrls.has(imgSrc) + ? { type: 'image', src: imgSrc } + : { type: 'fallback' }; + } + + const cached = avatarCache.get(userId); + if (cached && cached.avatar === userAvatar && cached.avatarSrc === avatarSrc) { + return cached.resolved; + } + + const imgSrc = userAvatar || avatarSrc; + const resolved: ResolvedAvatar = + imgSrc && !failedUrls.has(imgSrc) ? { type: 'image', src: imgSrc } : { type: 'fallback' }; + + avatarCache.set(userId, { avatar: userAvatar, avatarSrc, resolved }); + return resolved; +} + +function markAvatarFailed(userId: string, src: string): ResolvedAvatar { + failedUrls.add(src); + const fallback: ResolvedAvatar = { type: 'fallback' }; + const cached = avatarCache.get(userId); + if (cached) { + avatarCache.set(userId, { ...cached, resolved: fallback }); + } + return fallback; +} + type UserAvatarProps = { size: number; - user?: TUser; + avatar: string; avatarSrc: string; + userId: string; username: string; className?: string; }; -const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAvatarProps) => { - const [imageError, setImageError] = useState(false); +const UserAvatar = memo( + ({ size, avatar, avatarSrc, userId, username, className }: UserAvatarProps) => { + const [resolved, setResolved] = React.useState(() => resolveAvatar(userId, avatar, avatarSrc)); - const handleImageError = () => { - setImageError(true); - }; + React.useEffect(() => { + setResolved(resolveAvatar(userId, avatar, avatarSrc)); + }, [userId, avatar, avatarSrc]); - const renderDefaultAvatar = () => ( -
- -
- ); - - return ( -
- {(!(user?.avatar ?? '') && (!(user?.username ?? '') || user?.username.trim() === '')) || - imageError ? ( - renderDefaultAvatar() - ) : ( - avatar - )} -
- ); -}); + return ( +
+ {resolved.type === 'image' ? ( + avatar setResolved(markAvatarFailed(userId, resolved.src))} + /> + ) : ( +
+ +
+ )} +
+ ); + }, +); UserAvatar.displayName = 'UserAvatar'; @@ -74,9 +112,10 @@ const Icon: React.FC = memo((props) => { return ( ); diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx index a50b91c071..4114baefe4 100644 --- a/client/src/components/Messages/ContentRender.tsx +++ b/client/src/components/Messages/ContentRender.tsx @@ -144,7 +144,7 @@ const ContentRender = memo(function ContentRender({ )}
-
+
{messageLabel}
-
+
({ })); jest.mock('~/hooks/useLocalize', () => { - const fn = jest.fn((key: string) => key); + const fn = jest.fn((key: string) => key) as jest.Mock & { + TranslationKeys: Record; + }; fn.TranslationKeys = {}; return { __esModule: true, default: fn, TranslationKeys: {} }; }); @@ -87,6 +89,8 @@ jest.mock('../useUpdateFiles', () => ({ jest.mock('~/utils', () => ({ logger: { log: jest.fn() }, validateFiles: jest.fn(() => true), + cachePreview: jest.fn(), + getCachedPreview: jest.fn(() => undefined), })); const mockValidateFiles = jest.requireMock('~/utils').validateFiles; @@ -263,7 +267,7 @@ describe('useFileHandling', () => { it('falls back to "default" when no conversation endpoint and no override', async () => { mockConversation = { - conversationId: Constants.NEW_CONVO, + conversationId: Constants.NEW_CONVO as string, endpoint: null, endpointType: undefined, }; diff --git a/client/src/hooks/Files/useFileDeletion.ts b/client/src/hooks/Files/useFileDeletion.ts index 34d89e313b..c33ac2a50b 100644 --- a/client/src/hooks/Files/useFileDeletion.ts +++ b/client/src/hooks/Files/useFileDeletion.ts @@ -5,6 +5,7 @@ import type * as t from 'librechat-data-provider'; import type { UseMutateAsyncFunction } from '@tanstack/react-query'; import type { ExtendedFile, GenericSetter } from '~/common'; import useSetFilesToDelete from './useSetFilesToDelete'; +import { deletePreview } from '~/utils'; type FileMapSetter = GenericSetter>; @@ -88,6 +89,11 @@ const useFileDeletion = ({ }); } + deletePreview(file_id); + if (temp_file_id) { + deletePreview(temp_file_id); + } + if (attached) { return; } @@ -125,6 +131,11 @@ const useFileDeletion = ({ temp_file_id, embedded: embedded ?? false, }); + + deletePreview(file_id); + if (temp_file_id) { + deletePreview(temp_file_id); + } } if (setFiles) { diff --git a/client/src/hooks/Files/useFileHandling.ts b/client/src/hooks/Files/useFileHandling.ts index 68d56e75ad..be62700651 100644 --- a/client/src/hooks/Files/useFileHandling.ts +++ b/client/src/hooks/Files/useFileHandling.ts @@ -16,13 +16,13 @@ import debounce from 'lodash/debounce'; import type { EModelEndpoint, TEndpointsConfig, TError } from 'librechat-data-provider'; import type { ExtendedFile, FileSetter } from '~/common'; import type { TConversation } from 'librechat-data-provider'; +import { logger, validateFiles, cachePreview, getCachedPreview, removePreviewEntry } from '~/utils'; import { useGetFileConfig, useUploadFileMutation } from '~/data-provider'; import useLocalize, { TranslationKeys } from '~/hooks/useLocalize'; import { useDelayedUploadToast } from './useDelayedUploadToast'; import { processFileForUpload } from '~/utils/heicConverter'; import { useChatContext } from '~/Providers/ChatContext'; import { ephemeralAgentByConvoId } from '~/store'; -import { logger, validateFiles } from '~/utils'; import useClientResize from './useClientResize'; import useUpdateFiles from './useUpdateFiles'; @@ -130,6 +130,11 @@ const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: Fil ); setTimeout(() => { + const cachedBlob = getCachedPreview(data.temp_file_id); + if (cachedBlob && data.file_id !== data.temp_file_id) { + cachePreview(data.file_id, cachedBlob); + removePreviewEntry(data.temp_file_id); + } updateFileById( data.temp_file_id, { @@ -260,7 +265,6 @@ const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: Fil replaceFile(extendedFile); await startUpload(extendedFile); - URL.revokeObjectURL(preview); }; img.src = preview; }; @@ -301,6 +305,7 @@ const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: Fil try { // Create initial preview with original file const initialPreview = URL.createObjectURL(originalFile); + cachePreview(file_id, initialPreview); // Create initial ExtendedFile to show immediately const initialExtendedFile: ExtendedFile = { @@ -378,6 +383,7 @@ const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: Fil if (finalProcessedFile !== originalFile) { URL.revokeObjectURL(initialPreview); // Clean up original preview const newPreview = URL.createObjectURL(finalProcessedFile); + cachePreview(file_id, newPreview); const updatedExtendedFile: ExtendedFile = { ...initialExtendedFile, diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index 9f809bd6c1..325ee97315 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -526,6 +526,23 @@ export default function useEventHandlers({ } else if (requestMessage != null && responseMessage != null) { finalMessages = [...messages, requestMessage, responseMessage]; } + + /* Preserve files from current messages when server response lacks them */ + if (finalMessages.length > 0) { + const currentMsgMap = new Map( + currentMessages + .filter((m) => m.files && m.files.length > 0) + .map((m) => [m.messageId, m.files]), + ); + for (let i = 0; i < finalMessages.length; i++) { + const msg = finalMessages[i]; + const preservedFiles = currentMsgMap.get(msg.messageId); + if (msg.files == null && preservedFiles) { + finalMessages[i] = { ...msg, files: preservedFiles }; + } + } + } + if (finalMessages.length > 0) { setFinalMessages(conversation.conversationId, finalMessages); } else if ( diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts index 2a7dfc4a88..dae075b471 100644 --- a/client/src/utils/index.ts +++ b/client/src/utils/index.ts @@ -24,6 +24,7 @@ export * from './resources'; export * from './roles'; export * from './localStorage'; export * from './promptGroups'; +export * from './previewCache'; export * from './email'; export * from './share'; export * from './timestamps'; diff --git a/client/src/utils/previewCache.ts b/client/src/utils/previewCache.ts new file mode 100644 index 0000000000..604ce56308 --- /dev/null +++ b/client/src/utils/previewCache.ts @@ -0,0 +1,35 @@ +/** + * Module-level cache for local blob preview URLs keyed by file_id. + * Survives message replacements from SSE but clears on page refresh. + */ +const previewCache = new Map(); + +export function cachePreview(fileId: string, previewUrl: string): void { + const existing = previewCache.get(fileId); + if (existing && existing !== previewUrl) { + URL.revokeObjectURL(existing); + } + previewCache.set(fileId, previewUrl); +} + +export function getCachedPreview(fileId: string): string | undefined { + return previewCache.get(fileId); +} + +/** Removes the cache entry without revoking the blob (used when transferring between keys) */ +export function removePreviewEntry(fileId: string): void { + previewCache.delete(fileId); +} + +export function deletePreview(fileId: string): void { + const url = previewCache.get(fileId); + if (url) { + URL.revokeObjectURL(url); + previewCache.delete(fileId); + } +} + +export function clearPreviewCache(): void { + previewCache.forEach((url) => URL.revokeObjectURL(url)); + previewCache.clear(); +} diff --git a/package-lock.json b/package-lock.json index e634f9f053..f382df6b37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -445,7 +445,6 @@ "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", "react-i18next": "^15.4.0", - "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^3.0.6", "react-router-dom": "^6.30.3", @@ -500,6 +499,7 @@ "jest-environment-jsdom": "^30.2.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", + "monaco-editor": "^0.55.0", "postcss": "^8.4.31", "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", @@ -32359,11 +32359,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -34312,7 +34307,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -34323,7 +34317,6 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -34333,7 +34326,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -38142,19 +38134,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, - "node_modules/react-lazy-load-image-component": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.0.tgz", - "integrity": "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ==", - "dependencies": { - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1" - }, - "peerDependencies": { - "react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", - "react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x" - } - }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", From cfaa6337c100aa6f400164cf0373d4a0ffebcaaf Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 6 Mar 2026 19:18:35 -0500 Subject: [PATCH 006/111] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20`express?= =?UTF-8?q?-rate-limit`=20to=20v8.3.0=20(#12115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/package.json b/api/package.json index 7710956701..2f4bca3a0c 100644 --- a/api/package.json +++ b/api/package.json @@ -63,7 +63,7 @@ "eventsource": "^3.0.2", "express": "^5.2.1", "express-mongo-sanitize": "^2.2.0", - "express-rate-limit": "^8.2.1", + "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", "file-type": "^18.7.0", diff --git a/package-lock.json b/package-lock.json index f382df6b37..53e0b2e642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,7 +78,7 @@ "eventsource": "^3.0.2", "express": "^5.2.1", "express-mongo-sanitize": "^2.2.0", - "express-rate-limit": "^8.2.1", + "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", "file-type": "^18.7.0", @@ -27109,12 +27109,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -29284,9 +29284,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" From 2ac62a2e71376163eb1718676ee7d9c405470696 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 7 Mar 2026 10:45:43 -0500 Subject: [PATCH 007/111] =?UTF-8?q?=E2=9B=B5=20fix:=20Resolve=20Agent=20Pr?= =?UTF-8?q?ovider=20Endpoint=20Type=20for=20File=20Upload=20Support=20(#12?= =?UTF-8?q?117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Remove unused setValueOnChange prop from MCPServerMenuItem component * fix: Resolve agent provider endpoint type for file upload support When using the agents endpoint with a custom provider (e.g., Moonshot), the endpointType was resolving to "agents" instead of the provider's actual type ("custom"), causing "Upload to Provider" to not appear in the file attach menu. Adds `resolveEndpointType` utility in data-provider that follows the chain: endpoint (if not agents) → agent.provider → agents. Applied consistently across AttachFileChat, DragDropContext, useDragHelpers, and AgentPanel file components (FileContext, FileSearch, Code/Files). * refactor: Extract useAgentFileConfig hook, restore deleted tests, fix review findings - Extract shared provider resolution logic into useAgentFileConfig hook (Finding #2: DRY violation across FileContext, FileSearch, Code/Files) - Restore 18 deleted test cases in AttachFileMenu.spec.tsx covering agent capabilities, SharePoint, edge cases, and button state (Finding #1: accidental test deletion) - Wrap fileConfigEndpoint in useMemo in AttachFileChat (Finding #3) - Fix misleading test name in AgentFileConfig.spec.tsx (Finding #4) - Fix import order in FileSearch.tsx, FileContext.tsx, Code/Files.tsx (Finding #5) - Add comment about cache gap in useDragHelpers (Finding #6) - Clarify resolveEndpointType JSDoc (Finding #7) * refactor: Memoize Footer component for performance optimization - Converted Footer component to a memoized version to prevent unnecessary re-renders. - Improved import structure by adding memo to the React import statement for clarity. * chore: Fix remaining review nits - Widen useAgentFileConfig return type to EModelEndpoint | string - Fix import order in FileContext.tsx and FileSearch.tsx - Remove dead endpointType param from setupMocks in AttachFileMenu test * fix: Pass resolved provider endpoint to file upload validation AgentPanel file components (FileContext, FileSearch, Code/Files) were hardcoding endpointOverride to "agents", causing both client-side validation (file limits, MIME types) and server-side validation to use the agents config instead of the provider-specific config. Adds endpointTypeOverride to UseFileHandling params so endpoint and endpointType can be set independently. Components now pass the resolved provider name and type from useAgentFileConfig, so the full fallback chain (provider → custom → agents → default) applies to file upload validation on both client and server. * test: Verify any custom endpoint is document-supported regardless of name Adds parameterized tests with arbitrary endpoint names (spaces, hyphens, colons, etc.) confirming that all custom endpoints resolve to document-supported through resolveEndpointType, both as direct endpoints and as agent providers. * fix: Use || for provider fallback, test endpointOverride wiring - Change providerValue ?? to providerValue || so empty string is treated as "no provider" consistently with resolveEndpointType - Add wiring tests to CodeFiles, FileContext, FileSearch verifying endpointOverride and endpointTypeOverride are passed correctly - Update endpointOverride JSDoc to document endpointType fallback --- client/src/Providers/DragDropContext.tsx | 25 +- .../__tests__/DragDropContext.spec.tsx | 134 ++++ client/src/components/Chat/Footer.tsx | 11 +- .../Chat/Input/Files/AttachFileChat.tsx | 29 +- .../Chat/Input/Files/AttachFileMenu.tsx | 2 +- .../Files/__tests__/AttachFileChat.spec.tsx | 176 +++++ .../Files/__tests__/AttachFileMenu.spec.tsx | 675 +++++------------- .../src/components/MCP/MCPServerMenuItem.tsx | 1 - .../SidePanel/Agents/Code/Files.tsx | 25 +- .../SidePanel/Agents/FileContext.tsx | 35 +- .../SidePanel/Agents/FileSearch.tsx | 34 +- .../Agents/__tests__/AgentFileConfig.spec.tsx | 151 ++++ .../Agents/__tests__/CodeFiles.spec.tsx | 138 ++++ .../Agents/__tests__/FileContext.spec.tsx | 151 ++++ .../Agents/__tests__/FileSearch.spec.tsx | 147 ++++ client/src/hooks/Agents/index.ts | 1 + client/src/hooks/Agents/useAgentFileConfig.ts | 36 + client/src/hooks/Files/useDragHelpers.ts | 21 +- client/src/hooks/Files/useFileHandling.ts | 11 +- .../hooks/Files/useSharePointFileHandling.ts | 3 +- packages/data-provider/src/config.spec.ts | 315 ++++++++ packages/data-provider/src/config.ts | 34 +- 22 files changed, 1573 insertions(+), 582 deletions(-) create mode 100644 client/src/Providers/__tests__/DragDropContext.spec.tsx create mode 100644 client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/__tests__/CodeFiles.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/__tests__/FileContext.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/__tests__/FileSearch.spec.tsx create mode 100644 client/src/hooks/Agents/useAgentFileConfig.ts create mode 100644 packages/data-provider/src/config.spec.ts diff --git a/client/src/Providers/DragDropContext.tsx b/client/src/Providers/DragDropContext.tsx index e5a2177f2d..b519c0171f 100644 --- a/client/src/Providers/DragDropContext.tsx +++ b/client/src/Providers/DragDropContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useMemo } from 'react'; -import { getEndpointField, isAgentsEndpoint } from 'librechat-data-provider'; +import { isAgentsEndpoint, resolveEndpointType } from 'librechat-data-provider'; import type { EModelEndpoint } from 'librechat-data-provider'; import { useGetEndpointsQuery, useGetAgentByIdQuery } from '~/data-provider'; import { useAgentsMapContext } from './AgentsMapContext'; @@ -9,7 +9,7 @@ interface DragDropContextValue { conversationId: string | null | undefined; agentId: string | null | undefined; endpoint: string | null | undefined; - endpointType?: EModelEndpoint | undefined; + endpointType?: EModelEndpoint | string | undefined; useResponsesApi?: boolean; } @@ -20,13 +20,6 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { const { data: endpointsConfig } = useGetEndpointsQuery(); const agentsMap = useAgentsMapContext(); - const endpointType = useMemo(() => { - return ( - getEndpointField(endpointsConfig, conversation?.endpoint, 'type') || - (conversation?.endpoint as EModelEndpoint | undefined) - ); - }, [conversation?.endpoint, endpointsConfig]); - const needsAgentFetch = useMemo(() => { const isAgents = isAgentsEndpoint(conversation?.endpoint); if (!isAgents || !conversation?.agent_id) { @@ -40,6 +33,20 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { enabled: needsAgentFetch, }); + const agentProvider = useMemo(() => { + const isAgents = isAgentsEndpoint(conversation?.endpoint); + if (!isAgents || !conversation?.agent_id) { + return undefined; + } + const agent = agentData || agentsMap?.[conversation.agent_id]; + return agent?.provider; + }, [conversation?.endpoint, conversation?.agent_id, agentData, agentsMap]); + + const endpointType = useMemo( + () => resolveEndpointType(endpointsConfig, conversation?.endpoint, agentProvider), + [endpointsConfig, conversation?.endpoint, agentProvider], + ); + const useResponsesApi = useMemo(() => { const isAgents = isAgentsEndpoint(conversation?.endpoint); if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi) { diff --git a/client/src/Providers/__tests__/DragDropContext.spec.tsx b/client/src/Providers/__tests__/DragDropContext.spec.tsx new file mode 100644 index 0000000000..3c5e0f0796 --- /dev/null +++ b/client/src/Providers/__tests__/DragDropContext.spec.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react'; +import { EModelEndpoint } from 'librechat-data-provider'; +import type { TEndpointsConfig, Agent } from 'librechat-data-provider'; +import { DragDropProvider, useDragDropContext } from '../DragDropContext'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { userProvide: false, order: 0 }, + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + [EModelEndpoint.anthropic]: { userProvide: false, order: 6 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, + 'Some Endpoint': { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockConversation: Record | null = null; +let mockAgentsMap: Record> = {}; +let mockAgentQueryData: Partial | undefined; + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetAgentByIdQuery: () => ({ data: mockAgentQueryData }), +})); + +jest.mock('../AgentsMapContext', () => ({ + useAgentsMapContext: () => mockAgentsMap, +})); + +jest.mock('../ChatContext', () => ({ + useChatContext: () => ({ conversation: mockConversation }), +})); + +function wrapper({ children }: { children: React.ReactNode }) { + return {children}; +} + +describe('DragDropContext endpointType resolution', () => { + beforeEach(() => { + mockConversation = null; + mockAgentsMap = {}; + mockAgentQueryData = undefined; + }); + + describe('non-agents endpoints', () => { + it('resolves custom endpoint type for a custom endpoint', () => { + mockConversation = { endpoint: 'Moonshot' }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + + it('resolves endpoint name for a standard endpoint', () => { + mockConversation = { endpoint: EModelEndpoint.openAI }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.openAI); + }); + }); + + describe('agents endpoint with provider from agentsMap', () => { + it('resolves to custom for agent with Moonshot provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + + it('resolves to custom for agent with custom provider with spaces', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: 'Some Endpoint', model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + + it('resolves to openAI for agent with openAI provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.openAI); + }); + + it('resolves to anthropic for agent with anthropic provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.anthropic, model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.anthropic); + }); + }); + + describe('agents endpoint with provider from agentData query', () => { + it('uses agentData when agent is not in agentsMap', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-2' }; + mockAgentsMap = {}; + mockAgentQueryData = { provider: 'Moonshot' } as Partial; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + }); + + describe('agents endpoint without provider', () => { + it('falls back to agents when no agent_id', () => { + mockConversation = { endpoint: EModelEndpoint.agents }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.agents); + }); + + it('falls back to agents when agent has no provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { 'agent-1': { model_parameters: {} } as Partial }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.agents); + }); + }); + + describe('consistency: same endpoint type whether used directly or through agents', () => { + it('Moonshot resolves to the same type as direct endpoint and as agent provider', () => { + mockConversation = { endpoint: 'Moonshot' }; + const { result: directResult } = renderHook(() => useDragDropContext(), { wrapper }); + + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + const { result: agentResult } = renderHook(() => useDragDropContext(), { wrapper }); + + expect(directResult.current.endpointType).toBe(agentResult.current.endpointType); + }); + }); +}); diff --git a/client/src/components/Chat/Footer.tsx b/client/src/components/Chat/Footer.tsx index 75dd853c4f..541647a8d0 100644 --- a/client/src/components/Chat/Footer.tsx +++ b/client/src/components/Chat/Footer.tsx @@ -1,11 +1,11 @@ -import React, { useEffect } from 'react'; -import ReactMarkdown from 'react-markdown'; +import React, { useEffect, memo } from 'react'; import TagManager from 'react-gtm-module'; +import ReactMarkdown from 'react-markdown'; import { Constants } from 'librechat-data-provider'; import { useGetStartupConfig } from '~/data-provider'; import { useLocalize } from '~/hooks'; -export default function Footer({ className }: { className?: string }) { +function Footer({ className }: { className?: string }) { const { data: config } = useGetStartupConfig(); const localize = useLocalize(); @@ -98,3 +98,8 @@ export default function Footer({ className }: { className?: string }) {
); } + +const MemoizedFooter = memo(Footer); +MemoizedFooter.displayName = 'Footer'; + +export default MemoizedFooter; diff --git a/client/src/components/Chat/Input/Files/AttachFileChat.tsx b/client/src/components/Chat/Input/Files/AttachFileChat.tsx index 37b3584d3e..00a0b7aaa8 100644 --- a/client/src/components/Chat/Input/Files/AttachFileChat.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileChat.tsx @@ -2,10 +2,9 @@ import { memo, useMemo } from 'react'; import { Constants, supportsFiles, - EModelEndpoint, mergeFileConfig, isAgentsEndpoint, - getEndpointField, + resolveEndpointType, isAssistantsEndpoint, getEndpointFileConfig, } from 'librechat-data-provider'; @@ -55,21 +54,31 @@ function AttachFileChat({ const { data: endpointsConfig } = useGetEndpointsQuery(); - const endpointType = useMemo(() => { - return ( - getEndpointField(endpointsConfig, endpoint, 'type') || - (endpoint as EModelEndpoint | undefined) - ); - }, [endpoint, endpointsConfig]); + const agentProvider = useMemo(() => { + if (!isAgents || !conversation?.agent_id) { + return undefined; + } + const agent = agentData || agentsMap?.[conversation.agent_id]; + return agent?.provider; + }, [isAgents, conversation?.agent_id, agentData, agentsMap]); + const endpointType = useMemo( + () => resolveEndpointType(endpointsConfig, endpoint, agentProvider), + [endpointsConfig, endpoint, agentProvider], + ); + + const fileConfigEndpoint = useMemo( + () => (isAgents && agentProvider ? agentProvider : endpoint), + [isAgents, agentProvider, endpoint], + ); const endpointFileConfig = useMemo( () => getEndpointFileConfig({ - endpoint, fileConfig, endpointType, + endpoint: fileConfigEndpoint, }), - [endpoint, fileConfig, endpointType], + [fileConfigEndpoint, fileConfig, endpointType], ); const endpointSupportsFiles: boolean = useMemo( () => supportsFiles[endpointType ?? endpoint ?? ''] ?? false, diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 5b7346f646..62072e49e5 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -50,7 +50,7 @@ interface AttachFileMenuProps { endpoint?: string | null; disabled?: boolean | null; conversationId: string; - endpointType?: EModelEndpoint; + endpointType?: EModelEndpoint | string; endpointFileConfig?: EndpointFileConfig; useResponsesApi?: boolean; } diff --git a/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx new file mode 100644 index 0000000000..d12c25c4a3 --- /dev/null +++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig, Agent } from 'librechat-data-provider'; +import AttachFileChat from '../AttachFileChat'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { userProvide: false, order: 0 }, + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + [EModelEndpoint.assistants]: { userProvide: false, order: 2 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +const mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { fileLimit: 5 }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, +}); + +let mockAgentsMap: Record> = {}; +let mockAgentQueryData: Partial | undefined; + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (data: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), + useGetAgentByIdQuery: () => ({ data: mockAgentQueryData }), +})); + +jest.mock('~/Providers', () => ({ + useAgentsMapContext: () => mockAgentsMap, +})); + +/** Capture the props passed to AttachFileMenu */ +let mockAttachFileMenuProps: Record = {}; +jest.mock('../AttachFileMenu', () => { + return function MockAttachFileMenu(props: Record) { + mockAttachFileMenuProps = props; + return
; + }; +}); + +jest.mock('../AttachFile', () => { + return function MockAttachFile() { + return
; + }; +}); + +const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + +function renderComponent(conversation: Record | null, disableInputs = false) { + return render( + + + + + , + ); +} + +describe('AttachFileChat', () => { + beforeEach(() => { + mockAgentsMap = {}; + mockAgentQueryData = undefined; + mockAttachFileMenuProps = {}; + }); + + describe('rendering decisions', () => { + it('renders AttachFileMenu for agents endpoint', () => { + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(screen.getByTestId('attach-file-menu')).toBeInTheDocument(); + }); + + it('renders AttachFileMenu for custom endpoint with file support', () => { + renderComponent({ endpoint: 'Moonshot' }); + expect(screen.getByTestId('attach-file-menu')).toBeInTheDocument(); + }); + + it('renders null for null conversation', () => { + const { container } = renderComponent(null); + expect(container.innerHTML).toBe(''); + }); + }); + + describe('endpointType resolution for agents', () => { + it('passes custom endpointType when agent provider is a custom endpoint', () => { + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.custom); + }); + + it('passes openAI endpointType when agent provider is openAI', () => { + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.openAI); + }); + + it('passes agents endpointType when no agent provider', () => { + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.agents); + }); + + it('passes agents endpointType when no agent_id', () => { + renderComponent({ endpoint: EModelEndpoint.agents }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.agents); + }); + + it('uses agentData query when agent not in agentsMap', () => { + mockAgentQueryData = { provider: 'Moonshot' } as Partial; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-2' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.custom); + }); + }); + + describe('endpointType resolution for non-agents', () => { + it('passes custom endpointType for a custom endpoint', () => { + renderComponent({ endpoint: 'Moonshot' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.custom); + }); + + it('passes openAI endpointType for openAI endpoint', () => { + renderComponent({ endpoint: EModelEndpoint.openAI }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.openAI); + }); + }); + + describe('consistency: same endpoint type for direct vs agent usage', () => { + it('resolves Moonshot the same way whether used directly or through an agent', () => { + renderComponent({ endpoint: 'Moonshot' }); + const directType = mockAttachFileMenuProps.endpointType; + + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + const agentType = mockAttachFileMenuProps.endpointType; + + expect(directType).toBe(agentType); + }); + }); + + describe('endpointFileConfig resolution', () => { + it('passes Moonshot-specific file config for agent with Moonshot provider', () => { + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + const config = mockAttachFileMenuProps.endpointFileConfig as { fileLimit?: number }; + expect(config?.fileLimit).toBe(5); + }); + + it('passes agents file config when agent has no specific provider config', () => { + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + const config = mockAttachFileMenuProps.endpointFileConfig as { fileLimit?: number }; + expect(config?.fileLimit).toBe(10); + }); + + it('passes agents file config when no agent provider', () => { + renderComponent({ endpoint: EModelEndpoint.agents }); + const config = mockAttachFileMenuProps.endpointFileConfig as { fileLimit?: number }; + expect(config?.fileLimit).toBe(20); + }); + }); +}); diff --git a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx index d3f0fb65bc..cf08721207 100644 --- a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; import { RecoilRoot } from 'recoil'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { EModelEndpoint } from 'librechat-data-provider'; +import { EModelEndpoint, Providers } from 'librechat-data-provider'; import AttachFileMenu from '../AttachFileMenu'; -// Mock all the hooks jest.mock('~/hooks', () => ({ useAgentToolPermissions: jest.fn(), useAgentCapabilities: jest.fn(), @@ -25,53 +23,45 @@ jest.mock('~/data-provider', () => ({ })); jest.mock('~/components/SharePoint', () => ({ - SharePointPickerDialog: jest.fn(() => null), + SharePointPickerDialog: () => null, })); jest.mock('@librechat/client', () => { - const React = jest.requireActual('react'); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const R = require('react'); return { - FileUpload: React.forwardRef(({ children, handleFileChange }: any, ref: any) => ( -
- - {children} -
- )), - TooltipAnchor: ({ render }: any) => render, - DropdownPopup: ({ trigger, items, isOpen, setIsOpen }: any) => { - const handleTriggerClick = () => { - if (setIsOpen) { - setIsOpen(!isOpen); - } - }; - - return ( -
-
{trigger}
- {isOpen && ( -
- {items.map((item: any, idx: number) => ( - - ))} -
- )} -
- ); - }, - AttachmentIcon: () => 📎, - SharePointIcon: () => SP, + FileUpload: (props) => R.createElement('div', { 'data-testid': 'file-upload' }, props.children), + TooltipAnchor: (props) => props.render, + DropdownPopup: (props) => + R.createElement( + 'div', + null, + R.createElement('div', { onClick: () => props.setIsOpen(!props.isOpen) }, props.trigger), + props.isOpen && + R.createElement( + 'div', + { 'data-testid': 'dropdown-menu' }, + props.items.map((item, idx) => + R.createElement( + 'button', + { key: idx, onClick: item.onClick, 'data-testid': `menu-item-${idx}` }, + item.label, + ), + ), + ), + ), + AttachmentIcon: () => R.createElement('span', { 'data-testid': 'attachment-icon' }), + SharePointIcon: () => R.createElement('span', { 'data-testid': 'sharepoint-icon' }), }; }); -jest.mock('@ariakit/react', () => ({ - MenuButton: ({ children, onClick, disabled, ...props }: any) => ( - - ), -})); +jest.mock('@ariakit/react', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const R = require('react'); + return { + MenuButton: (props) => R.createElement('button', props, props.children), + }; +}); const mockUseAgentToolPermissions = jest.requireMock('~/hooks').useAgentToolPermissions; const mockUseAgentCapabilities = jest.requireMock('~/hooks').useAgentCapabilities; @@ -83,558 +73,283 @@ const mockUseSharePointFileHandling = jest.requireMock( ).default; const mockUseGetStartupConfig = jest.requireMock('~/data-provider').useGetStartupConfig; -describe('AttachFileMenu', () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - }, - }); +const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - const mockHandleFileChange = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - - // Default mock implementations - mockUseLocalize.mockReturnValue((key: string) => { - const translations: Record = { - com_ui_upload_provider: 'Upload to Provider', - com_ui_upload_image_input: 'Upload Image', - com_ui_upload_ocr_text: 'Upload OCR Text', - com_ui_upload_file_search: 'Upload for File Search', - com_ui_upload_code_files: 'Upload Code Files', - com_sidepanel_attach_files: 'Attach Files', - com_files_upload_sharepoint: 'Upload from SharePoint', - }; - return translations[key] || key; - }); - - mockUseAgentCapabilities.mockReturnValue({ - contextEnabled: false, - fileSearchEnabled: false, - codeEnabled: false, - }); - - mockUseGetAgentsConfig.mockReturnValue({ - agentsConfig: { - capabilities: { - contextEnabled: false, - fileSearchEnabled: false, - codeEnabled: false, - }, - }, - }); - - mockUseFileHandling.mockReturnValue({ - handleFileChange: mockHandleFileChange, - }); - - mockUseSharePointFileHandling.mockReturnValue({ - handleSharePointFiles: jest.fn(), - isProcessing: false, - downloadProgress: 0, - }); - - mockUseGetStartupConfig.mockReturnValue({ - data: { - sharePointFilePickerEnabled: false, - }, - }); - - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: undefined, - }); - }); - - const renderAttachFileMenu = (props: any = {}) => { - return render( - - - - - , - ); +function setupMocks(overrides: { provider?: string } = {}) { + const translations: Record = { + com_ui_upload_provider: 'Upload to Provider', + com_ui_upload_image_input: 'Upload Image', + com_ui_upload_ocr_text: 'Upload as Text', + com_ui_upload_file_search: 'Upload for File Search', + com_ui_upload_code_files: 'Upload Code Files', + com_sidepanel_attach_files: 'Attach Files', + com_files_upload_sharepoint: 'Upload from SharePoint', }; - - describe('Basic Rendering', () => { - it('should render the attachment button', () => { - renderAttachFileMenu(); - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); - }); - - it('should be disabled when disabled prop is true', () => { - renderAttachFileMenu({ disabled: true }); - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeDisabled(); - }); - - it('should not be disabled when disabled prop is false', () => { - renderAttachFileMenu({ disabled: false }); - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).not.toBeDisabled(); - }); + mockUseLocalize.mockReturnValue((key: string) => translations[key] || key); + mockUseAgentCapabilities.mockReturnValue({ + contextEnabled: false, + fileSearchEnabled: false, + codeEnabled: false, }); + mockUseGetAgentsConfig.mockReturnValue({ agentsConfig: {} }); + mockUseFileHandling.mockReturnValue({ handleFileChange: jest.fn() }); + mockUseSharePointFileHandling.mockReturnValue({ + handleSharePointFiles: jest.fn(), + isProcessing: false, + downloadProgress: 0, + }); + mockUseGetStartupConfig.mockReturnValue({ data: { sharePointFilePickerEnabled: false } }); + mockUseAgentToolPermissions.mockReturnValue({ + fileSearchAllowedByAgent: false, + codeAllowedByAgent: false, + provider: overrides.provider ?? undefined, + }); +} - describe('Provider Detection Fix - endpointType Priority', () => { - it('should prioritize endpointType over currentProvider for LiteLLM gateway', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: 'litellm', // Custom gateway name NOT in documentSupportedProviders - }); +function renderMenu(props: Record = {}) { + return render( + + + + + , + ); +} - renderAttachFileMenu({ - endpoint: 'litellm', - endpointType: EModelEndpoint.openAI, // Backend override IS in documentSupportedProviders - }); +function openMenu() { + fireEvent.click(screen.getByRole('button', { name: /attach file options/i })); +} - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); +describe('AttachFileMenu', () => { + beforeEach(jest.clearAllMocks); - // With the fix, should show "Upload to Provider" because endpointType is checked first + describe('Upload to Provider vs Upload Image', () => { + it('shows "Upload to Provider" when endpointType is custom (resolved from agent provider)', () => { + setupMocks({ provider: 'Moonshot' }); + renderMenu({ endpointType: EModelEndpoint.custom }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); expect(screen.queryByText('Upload Image')).not.toBeInTheDocument(); }); - it('should show Upload to Provider for custom endpoints with OpenAI endpointType', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: 'my-custom-gateway', - }); - - renderAttachFileMenu({ - endpoint: 'my-custom-gateway', - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + it('shows "Upload to Provider" when endpointType is openAI', () => { + setupMocks({ provider: EModelEndpoint.openAI }); + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); - it('should show Upload Image when neither endpointType nor provider support documents', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: 'unsupported-provider', - }); + it('shows "Upload to Provider" when endpointType is anthropic', () => { + setupMocks({ provider: EModelEndpoint.anthropic }); + renderMenu({ endpointType: EModelEndpoint.anthropic }); + openMenu(); + expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + }); - renderAttachFileMenu({ - endpoint: 'unsupported-provider', - endpointType: 'unsupported-endpoint' as any, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); + it('shows "Upload to Provider" when endpointType is google', () => { + setupMocks({ provider: Providers.GOOGLE }); + renderMenu({ endpointType: EModelEndpoint.google }); + openMenu(); + expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + }); + it('shows "Upload Image" when endpointType is agents (no provider resolution)', () => { + setupMocks(); + renderMenu({ endpointType: EModelEndpoint.agents }); + openMenu(); expect(screen.getByText('Upload Image')).toBeInTheDocument(); expect(screen.queryByText('Upload to Provider')).not.toBeInTheDocument(); }); - it('should fallback to currentProvider when endpointType is undefined', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.openAI, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.openAI, - endpointType: undefined, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); + it('shows "Upload Image" when neither endpointType nor provider supports documents', () => { + setupMocks({ provider: 'unknown-provider' }); + renderMenu({ endpointType: 'unknown-type' }); + openMenu(); + expect(screen.getByText('Upload Image')).toBeInTheDocument(); + }); + it('shows "Upload to Provider" for azureOpenAI with useResponsesApi', () => { + setupMocks({ provider: EModelEndpoint.azureOpenAI }); + renderMenu({ endpointType: EModelEndpoint.azureOpenAI, useResponsesApi: true }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); - it('should fallback to currentProvider when endpointType is null', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.anthropic, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.anthropic, - endpointType: null, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + it('shows "Upload Image" for azureOpenAI without useResponsesApi', () => { + setupMocks({ provider: EModelEndpoint.azureOpenAI }); + renderMenu({ endpointType: EModelEndpoint.azureOpenAI, useResponsesApi: false }); + openMenu(); + expect(screen.getByText('Upload Image')).toBeInTheDocument(); }); }); - describe('Supported Providers', () => { - const supportedProviders = [ - { name: 'OpenAI', endpoint: EModelEndpoint.openAI }, - { name: 'Anthropic', endpoint: EModelEndpoint.anthropic }, - { name: 'Google', endpoint: EModelEndpoint.google }, - { name: 'Custom', endpoint: EModelEndpoint.custom }, - ]; - - supportedProviders.forEach(({ name, endpoint }) => { - it(`should show Upload to Provider for ${name}`, () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: endpoint, - }); - - renderAttachFileMenu({ - endpoint, - endpointType: endpoint, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + describe('agent provider resolution scenario', () => { + it('shows "Upload to Provider" when agents endpoint has custom endpointType from provider', () => { + setupMocks({ provider: 'Moonshot' }); + renderMenu({ + endpoint: EModelEndpoint.agents, + endpointType: EModelEndpoint.custom, }); - }); - - it('should show Upload to Provider for Azure OpenAI with useResponsesApi', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.azureOpenAI, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.azureOpenAI, - endpointType: EModelEndpoint.azureOpenAI, - useResponsesApi: true, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); - it('should NOT show Upload to Provider for Azure OpenAI without useResponsesApi', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.azureOpenAI, + it('shows "Upload Image" when agents endpoint has no resolved provider type', () => { + setupMocks(); + renderMenu({ + endpoint: EModelEndpoint.agents, + endpointType: EModelEndpoint.agents, }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.azureOpenAI, - endpointType: EModelEndpoint.azureOpenAI, - useResponsesApi: false, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.queryByText('Upload to Provider')).not.toBeInTheDocument(); + openMenu(); expect(screen.getByText('Upload Image')).toBeInTheDocument(); }); }); + describe('Basic Rendering', () => { + it('renders the attachment button', () => { + setupMocks(); + renderMenu(); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); + }); + + it('is disabled when disabled prop is true', () => { + setupMocks(); + renderMenu({ disabled: true }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeDisabled(); + }); + + it('is not disabled when disabled prop is false', () => { + setupMocks(); + renderMenu({ disabled: false }); + expect(screen.getByRole('button', { name: /attach file options/i })).not.toBeDisabled(); + }); + }); + describe('Agent Capabilities', () => { - it('should show OCR Text option when context is enabled', () => { + it('shows OCR Text option when context is enabled', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: true, fileSearchEnabled: false, codeEnabled: false, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload OCR Text')).toBeInTheDocument(); + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); + expect(screen.getByText('Upload as Text')).toBeInTheDocument(); }); - it('should show File Search option when enabled and allowed by agent', () => { + it('shows File Search option when enabled and allowed by agent', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: false, fileSearchEnabled: true, codeEnabled: false, }); - mockUseAgentToolPermissions.mockReturnValue({ fileSearchAllowedByAgent: true, codeAllowedByAgent: false, provider: undefined, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload for File Search')).toBeInTheDocument(); }); - it('should NOT show File Search when enabled but not allowed by agent', () => { + it('does NOT show File Search when enabled but not allowed by agent', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: false, fileSearchEnabled: true, codeEnabled: false, }); - - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: undefined, - }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.queryByText('Upload for File Search')).not.toBeInTheDocument(); }); - it('should show Code Files option when enabled and allowed by agent', () => { + it('shows Code Files option when enabled and allowed by agent', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: false, fileSearchEnabled: false, codeEnabled: true, }); - mockUseAgentToolPermissions.mockReturnValue({ fileSearchAllowedByAgent: false, codeAllowedByAgent: true, provider: undefined, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload Code Files')).toBeInTheDocument(); }); - it('should show all options when all capabilities are enabled', () => { + it('shows all options when all capabilities are enabled', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: true, fileSearchEnabled: true, codeEnabled: true, }); - mockUseAgentToolPermissions.mockReturnValue({ fileSearchAllowedByAgent: true, codeAllowedByAgent: true, provider: undefined, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); - expect(screen.getByText('Upload OCR Text')).toBeInTheDocument(); + expect(screen.getByText('Upload as Text')).toBeInTheDocument(); expect(screen.getByText('Upload for File Search')).toBeInTheDocument(); expect(screen.getByText('Upload Code Files')).toBeInTheDocument(); }); }); describe('SharePoint Integration', () => { - it('should show SharePoint option when enabled', () => { + it('shows SharePoint option when enabled', () => { + setupMocks(); mockUseGetStartupConfig.mockReturnValue({ - data: { - sharePointFilePickerEnabled: true, - }, + data: { sharePointFilePickerEnabled: true }, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload from SharePoint')).toBeInTheDocument(); }); - it('should NOT show SharePoint option when disabled', () => { - mockUseGetStartupConfig.mockReturnValue({ - data: { - sharePointFilePickerEnabled: false, - }, - }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + it('does NOT show SharePoint option when disabled', () => { + setupMocks(); + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.queryByText('Upload from SharePoint')).not.toBeInTheDocument(); }); }); describe('Edge Cases', () => { - it('should handle undefined endpoint and provider gracefully', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: undefined, - }); - - renderAttachFileMenu({ - endpoint: undefined, - endpointType: undefined, - }); - + it('handles undefined endpoint and provider gracefully', () => { + setupMocks(); + renderMenu({ endpoint: undefined, endpointType: undefined }); const button = screen.getByRole('button', { name: /attach file options/i }); expect(button).toBeInTheDocument(); fireEvent.click(button); - - // Should show Upload Image as fallback expect(screen.getByText('Upload Image')).toBeInTheDocument(); }); - it('should handle null endpoint and provider gracefully', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: null, - }); - - renderAttachFileMenu({ - endpoint: null, - endpointType: null, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); + it('handles null endpoint and provider gracefully', () => { + setupMocks(); + renderMenu({ endpoint: null, endpointType: null }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); }); - it('should handle missing agentId gracefully', () => { - renderAttachFileMenu({ - agentId: undefined, - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); + it('handles missing agentId gracefully', () => { + setupMocks(); + renderMenu({ agentId: undefined, endpointType: EModelEndpoint.openAI }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); }); - it('should handle empty string agentId', () => { - renderAttachFileMenu({ - agentId: '', - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); - }); - }); - - describe('Google Provider Special Case', () => { - it('should use image_document_video_audio file type for Google provider', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.google, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.google, - endpointType: EModelEndpoint.google, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - const uploadProviderButton = screen.getByText('Upload to Provider'); - expect(uploadProviderButton).toBeInTheDocument(); - - // Click the upload to provider option - fireEvent.click(uploadProviderButton); - - // The file input should have been clicked (indirectly tested through the implementation) - }); - - it('should use image_document file type for non-Google providers', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.openAI, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.openAI, - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - const uploadProviderButton = screen.getByText('Upload to Provider'); - expect(uploadProviderButton).toBeInTheDocument(); - fireEvent.click(uploadProviderButton); - - // Implementation detail - image_document type is used - }); - }); - - describe('Regression Tests', () => { - it('should not break the previous behavior for direct provider attachments', () => { - // When using a direct supported provider (not through a gateway) - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.anthropic, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.anthropic, - endpointType: EModelEndpoint.anthropic, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); - }); - - it('should maintain correct priority when both are supported', () => { - // Both endpointType and provider are supported, endpointType should be checked first - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.google, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.google, - endpointType: EModelEndpoint.openAI, // Different but both supported - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - // Should still work because endpointType (openAI) is supported - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + it('handles empty string agentId', () => { + setupMocks(); + renderMenu({ agentId: '', endpointType: EModelEndpoint.openAI }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); }); }); }); diff --git a/client/src/components/MCP/MCPServerMenuItem.tsx b/client/src/components/MCP/MCPServerMenuItem.tsx index 2291a5233e..7fcb773bb9 100644 --- a/client/src/components/MCP/MCPServerMenuItem.tsx +++ b/client/src/components/MCP/MCPServerMenuItem.tsx @@ -46,7 +46,6 @@ export default function MCPServerMenuItem({ name="mcp-servers" value={server.serverName} checked={isSelected} - setValueOnChange={false} onChange={() => onToggle(server.serverName)} aria-label={accessibleLabel} className={cn( diff --git a/client/src/components/SidePanel/Agents/Code/Files.tsx b/client/src/components/SidePanel/Agents/Code/Files.tsx index 64524e66da..16360a7a0b 100644 --- a/client/src/components/SidePanel/Agents/Code/Files.tsx +++ b/client/src/components/SidePanel/Agents/Code/Files.tsx @@ -1,18 +1,11 @@ import { memo, useMemo, useRef, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { AttachmentIcon } from '@librechat/client'; -import { - EToolResources, - EModelEndpoint, - mergeFileConfig, - AgentCapabilities, - getEndpointFileConfig, -} from 'librechat-data-provider'; +import { EToolResources, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider'; import type { ExtendedFile, AgentForm } from '~/common'; import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling'; +import { useAgentFileConfig, useLocalize, useLazyEffect } from '~/hooks'; import FileRow from '~/components/Chat/Input/Files/FileRow'; -import { useLocalize, useLazyEffect } from '~/hooks'; -import { useGetFileConfig } from '~/data-provider'; import { isEphemeralAgent } from '~/common'; const tool_resource = EToolResources.execute_code; @@ -29,14 +22,14 @@ function Files({ const fileInputRef = useRef(null); const [files, setFiles] = useState>(new Map()); const fileHandlingState = useMemo(() => ({ files, setFiles, conversation: null }), [files]); - const { data: fileConfig = null } = useGetFileConfig({ - select: (data) => mergeFileConfig(data), - }); + const { endpointFileConfig, providerValue, endpointType } = useAgentFileConfig(); + const endpointOverride = providerValue || EModelEndpoint.agents; const { abortUpload, handleFileChange } = useFileHandlingNoChatContext( { fileSetter: setFiles, additionalMetadata: { agent_id, tool_resource }, - endpointOverride: EModelEndpoint.agents, + endpointOverride, + endpointTypeOverride: endpointType, }, fileHandlingState, ); @@ -52,12 +45,6 @@ function Files({ ); const codeChecked = watch(AgentCapabilities.execute_code); - - const endpointFileConfig = getEndpointFileConfig({ - fileConfig, - endpoint: EModelEndpoint.agents, - endpointType: EModelEndpoint.agents, - }); const isUploadDisabled = endpointFileConfig?.disabled ?? false; if (isUploadDisabled) { diff --git a/client/src/components/SidePanel/Agents/FileContext.tsx b/client/src/components/SidePanel/Agents/FileContext.tsx index 433992b1d0..906d742127 100644 --- a/client/src/components/SidePanel/Agents/FileContext.tsx +++ b/client/src/components/SidePanel/Agents/FileContext.tsx @@ -1,12 +1,7 @@ import { memo, useMemo, useRef, useState } from 'react'; import { Folder } from 'lucide-react'; import * as Ariakit from '@ariakit/react'; -import { - EModelEndpoint, - EToolResources, - mergeFileConfig, - getEndpointFileConfig, -} from 'librechat-data-provider'; +import { EModelEndpoint, EToolResources } from 'librechat-data-provider'; import { HoverCard, DropdownPopup, @@ -18,13 +13,13 @@ import { HoverCardTrigger, } from '@librechat/client'; import type { ExtendedFile } from '~/common'; -import { useLocalize, useLazyEffect } from '~/hooks'; -import { useGetFileConfig, useGetStartupConfig } from '~/data-provider'; -import { SharePointPickerDialog } from '~/components/SharePoint'; -import FileRow from '~/components/Chat/Input/Files/FileRow'; -import { ESide, isEphemeralAgent } from '~/common'; import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling'; import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling'; +import { useAgentFileConfig, useLocalize, useLazyEffect } from '~/hooks'; +import { SharePointPickerDialog } from '~/components/SharePoint'; +import FileRow from '~/components/Chat/Input/Files/FileRow'; +import { useGetStartupConfig } from '~/data-provider'; +import { ESide, isEphemeralAgent } from '~/common'; function FileContext({ agent_id, @@ -41,15 +36,14 @@ function FileContext({ const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false); const { data: startupConfig } = useGetStartupConfig(); const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled; - - const { data: fileConfig = null } = useGetFileConfig({ - select: (data) => mergeFileConfig(data), - }); + const { endpointFileConfig, providerValue, endpointType } = useAgentFileConfig(); + const endpointOverride = providerValue || EModelEndpoint.agents; const { handleFileChange } = useFileHandlingNoChatContext( { additionalMetadata: { agent_id, tool_resource: EToolResources.context }, - endpointOverride: EModelEndpoint.agents, + endpointOverride, + endpointTypeOverride: endpointType, fileSetter: setFiles, }, fileHandlingState, @@ -58,7 +52,8 @@ function FileContext({ useSharePointFileHandlingNoChatContext( { additionalMetadata: { agent_id, tool_resource: EToolResources.file_search }, - endpointOverride: EModelEndpoint.agents, + endpointOverride, + endpointTypeOverride: endpointType, fileSetter: setFiles, }, fileHandlingState, @@ -72,12 +67,6 @@ function FileContext({ [_files], 750, ); - - const endpointFileConfig = getEndpointFileConfig({ - fileConfig, - endpoint: EModelEndpoint.agents, - endpointType: EModelEndpoint.agents, - }); const isUploadDisabled = endpointFileConfig?.disabled ?? false; const handleSharePointFilesSelected = async (sharePointFiles: any[]) => { try { diff --git a/client/src/components/SidePanel/Agents/FileSearch.tsx b/client/src/components/SidePanel/Agents/FileSearch.tsx index bb7d272d90..79a08de0ed 100644 --- a/client/src/components/SidePanel/Agents/FileSearch.tsx +++ b/client/src/components/SidePanel/Agents/FileSearch.tsx @@ -3,22 +3,16 @@ import { Folder } from 'lucide-react'; import * as Ariakit from '@ariakit/react'; import { useFormContext } from 'react-hook-form'; import { SharePointIcon, AttachmentIcon, DropdownPopup } from '@librechat/client'; -import { - EModelEndpoint, - EToolResources, - mergeFileConfig, - AgentCapabilities, - getEndpointFileConfig, -} from 'librechat-data-provider'; +import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider'; import type { ExtendedFile, AgentForm } from '~/common'; -import { useGetFileConfig, useGetStartupConfig } from '~/data-provider'; -import { useLocalize, useLazyEffect } from '~/hooks'; +import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling'; +import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling'; +import { useAgentFileConfig, useLocalize, useLazyEffect } from '~/hooks'; import { SharePointPickerDialog } from '~/components/SharePoint'; import FileRow from '~/components/Chat/Input/Files/FileRow'; +import { useGetStartupConfig } from '~/data-provider'; import FileSearchCheckbox from './FileSearchCheckbox'; import { isEphemeralAgent } from '~/common'; -import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling'; -import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling'; function FileSearch({ agent_id, @@ -37,15 +31,14 @@ function FileSearch({ // Get startup configuration for SharePoint feature flag const { data: startupConfig } = useGetStartupConfig(); - - const { data: fileConfig = null } = useGetFileConfig({ - select: (data) => mergeFileConfig(data), - }); + const { endpointFileConfig, providerValue, endpointType } = useAgentFileConfig(); + const endpointOverride = providerValue || EModelEndpoint.agents; const { handleFileChange } = useFileHandlingNoChatContext( { additionalMetadata: { agent_id, tool_resource: EToolResources.file_search }, - endpointOverride: EModelEndpoint.agents, + endpointOverride, + endpointTypeOverride: endpointType, fileSetter: setFiles, }, fileHandlingState, @@ -55,7 +48,8 @@ function FileSearch({ useSharePointFileHandlingNoChatContext( { additionalMetadata: { agent_id, tool_resource: EToolResources.file_search }, - endpointOverride: EModelEndpoint.agents, + endpointOverride, + endpointTypeOverride: endpointType, fileSetter: setFiles, }, fileHandlingState, @@ -72,12 +66,6 @@ function FileSearch({ ); const fileSearchChecked = watch(AgentCapabilities.file_search); - - const endpointFileConfig = getEndpointFileConfig({ - fileConfig, - endpoint: EModelEndpoint.agents, - endpointType: EModelEndpoint.agents, - }); const isUploadDisabled = endpointFileConfig?.disabled ?? false; const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled; diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx new file mode 100644 index 0000000000..aeb0dd3ff9 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig, resolveEndpointType } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import useAgentFileConfig from '~/hooks/Agents/useAgentFileConfig'; + +/** + * Tests the useAgentFileConfig hook used by FileContext, FileSearch, and Code/Files. + * Uses the real hook with mocked data-fetching layer. + */ + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { userProvide: false, order: 0 }, + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, + 'Some Endpoint': { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { fileLimit: 5 }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, +}); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (data: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), +})); + +function FileConfigProbe() { + const { endpointType, endpointFileConfig } = useAgentFileConfig(); + return ( +
+ {String(endpointType)} + {endpointFileConfig.fileLimit} + {String(endpointFileConfig.disabled ?? false)} +
+ ); +} + +function TestWrapper({ provider }: { provider?: string | { label: string; value: string } }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return ( + + + + ); +} + +describe('AgentPanel file config resolution (useAgentFileConfig)', () => { + describe('endpointType resolution from form provider', () => { + it('resolves to custom when provider is a custom endpoint string', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom); + }); + + it('resolves to custom when provider is a custom endpoint with spaces', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom); + }); + + it('resolves to openAI when provider is openAI', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.openAI); + }); + + it('falls back to agents when provider is undefined', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents); + }); + + it('falls back to agents when provider is empty string', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('falls back to agents when provider option has empty value', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('resolves correctly when provider is an option object', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom); + }); + }); + + describe('file config fallback chain', () => { + it('uses Moonshot-specific file config when provider is Moonshot', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('5'); + }); + + it('falls back to agents file config when provider has no specific config', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('uses agents file config when no provider is set', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('falls back to default config for openAI provider (no openAI-specific config)', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('10'); + }); + }); + + describe('disabled state', () => { + it('reports not disabled for standard config', () => { + render(); + expect(screen.getByTestId('disabled').textContent).toBe('false'); + }); + + it('reports disabled when provider-specific config is disabled', () => { + const original = mockFileConfig; + mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { disabled: true }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + + render(); + expect(screen.getByTestId('disabled').textContent).toBe('true'); + + mockFileConfig = original; + }); + }); + + describe('consistency with direct custom endpoint', () => { + it('resolves to the same type as a direct custom endpoint would', () => { + render(); + const agentEndpointType = screen.getByTestId('endpointType').textContent; + const directEndpointType = resolveEndpointType(mockEndpointsConfig, 'Moonshot'); + expect(agentEndpointType).toBe(directEndpointType); + }); + }); +}); diff --git a/client/src/components/SidePanel/Agents/__tests__/CodeFiles.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/CodeFiles.spec.tsx new file mode 100644 index 0000000000..0e965e4c84 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/CodeFiles.spec.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import Files from '../Code/Files'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), +})); + +jest.mock('~/hooks', () => ({ + useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default, + useLocalize: () => (key: string) => key, + useLazyEffect: () => {}, +})); + +const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({ + abortUpload: jest.fn(), + handleFileChange: jest.fn(), +}); + +jest.mock('~/hooks/Files/useFileHandling', () => ({ + useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args), +})); + +jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null); + +jest.mock('@librechat/client', () => ({ + AttachmentIcon: () => , +})); + +function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return {children}; +} + +describe('Code/Files', () => { + it('renders upload UI when file uploads are not disabled', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + render( + + + , + ); + expect(screen.getByText('com_assistants_code_interpreter_files')).toBeInTheDocument(); + }); + + it('returns null when file config is disabled for provider', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('returns null when agents endpoint config is disabled and no provider config', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe('Moonshot'); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom); + }); + + it('falls back to agents for endpointOverride when no provider', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents); + }); + + it('falls back to agents for endpointOverride when provider is empty string', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + }); + + it('renders when provider has no specific config and agents config is enabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + render( + + + , + ); + expect(screen.getByText('com_assistants_code_interpreter_files')).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/SidePanel/Agents/__tests__/FileContext.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/FileContext.spec.tsx new file mode 100644 index 0000000000..f99d71d2b7 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/FileContext.spec.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import FileContext from '../FileContext'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), + useGetStartupConfig: () => ({ data: { sharePointFilePickerEnabled: false } }), +})); + +jest.mock('~/hooks', () => ({ + useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default, + useLocalize: () => (key: string) => key, + useLazyEffect: () => {}, +})); + +const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({ + handleFileChange: jest.fn(), +}); + +jest.mock('~/hooks/Files/useFileHandling', () => ({ + useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args), +})); + +jest.mock('~/hooks/Files/useSharePointFileHandling', () => ({ + useSharePointFileHandlingNoChatContext: () => ({ + handleSharePointFiles: jest.fn(), + isProcessing: false, + downloadProgress: 0, + }), +})); + +jest.mock('~/components/SharePoint', () => ({ + SharePointPickerDialog: () => null, +})); + +jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null); + +jest.mock('@ariakit/react', () => ({ + MenuButton: ({ children, ...props }: { children: React.ReactNode }) => ( + + ), +})); + +jest.mock('@librechat/client', () => ({ + HoverCard: ({ children }: { children: React.ReactNode }) =>
{children}
, + DropdownPopup: () => null, + AttachmentIcon: () => , + CircleHelpIcon: () => , + SharePointIcon: () => , + HoverCardPortal: ({ children }: { children: React.ReactNode }) =>
{children}
, + HoverCardContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + HoverCardTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return {children}; +} + +describe('FileContext', () => { + it('renders upload UI when file uploads are not disabled', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + render( + + + , + ); + expect(screen.getByText('com_agents_file_context_label')).toBeInTheDocument(); + }); + + it('returns null when file config is disabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('returns null when agents endpoint config is disabled and provider has no specific config', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe('Moonshot'); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom); + }); + + it('falls back to agents for endpointOverride when no provider', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents); + }); + + it('renders when provider has no specific config and agents config is enabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + render( + + + , + ); + expect(screen.getByText('com_agents_file_context_label')).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/SidePanel/Agents/__tests__/FileSearch.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/FileSearch.spec.tsx new file mode 100644 index 0000000000..003388f5d8 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/FileSearch.spec.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import FileSearch from '../FileSearch'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), + useGetStartupConfig: () => ({ data: { sharePointFilePickerEnabled: false } }), +})); + +jest.mock('~/hooks', () => ({ + useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default, + useLocalize: () => (key: string) => key, + useLazyEffect: () => {}, +})); + +const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({ + handleFileChange: jest.fn(), +}); + +jest.mock('~/hooks/Files/useFileHandling', () => ({ + useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args), +})); + +jest.mock('~/hooks/Files/useSharePointFileHandling', () => ({ + useSharePointFileHandlingNoChatContext: () => ({ + handleSharePointFiles: jest.fn(), + isProcessing: false, + downloadProgress: 0, + }), +})); + +jest.mock('~/components/SharePoint', () => ({ + SharePointPickerDialog: () => null, +})); + +jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null); +jest.mock('../FileSearchCheckbox', () => () => null); + +jest.mock('@ariakit/react', () => ({ + MenuButton: ({ children, ...props }: { children: React.ReactNode }) => ( + + ), +})); + +jest.mock('@librechat/client', () => ({ + SharePointIcon: () => , + AttachmentIcon: () => , + DropdownPopup: () => null, +})); + +function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return {children}; +} + +describe('FileSearch', () => { + it('renders upload UI when file uploads are not disabled', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + render( + + + , + ); + expect(screen.getByText('com_assistants_file_search')).toBeInTheDocument(); + }); + + it('returns null when file config is disabled for provider', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('returns null when agents endpoint config is disabled and no provider config', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe('Moonshot'); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom); + }); + + it('falls back to agents for endpointOverride when no provider', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents); + }); + + it('renders when provider has no specific config and agents config is enabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + render( + + + , + ); + expect(screen.getByText('com_assistants_file_search')).toBeInTheDocument(); + }); +}); diff --git a/client/src/hooks/Agents/index.ts b/client/src/hooks/Agents/index.ts index f75d045cc0..a553da24a0 100644 --- a/client/src/hooks/Agents/index.ts +++ b/client/src/hooks/Agents/index.ts @@ -5,6 +5,7 @@ export type { ProcessedAgentCategory } from './useAgentCategories'; export { default as useAgentCapabilities } from './useAgentCapabilities'; export { default as useGetAgentsConfig } from './useGetAgentsConfig'; export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel'; +export { default as useAgentFileConfig } from './useAgentFileConfig'; export { default as useAgentToolPermissions } from './useAgentToolPermissions'; export { default as useMCPToolOptions } from './useMCPToolOptions'; export * from './useApplyModelSpecAgents'; diff --git a/client/src/hooks/Agents/useAgentFileConfig.ts b/client/src/hooks/Agents/useAgentFileConfig.ts new file mode 100644 index 0000000000..7f98f8d575 --- /dev/null +++ b/client/src/hooks/Agents/useAgentFileConfig.ts @@ -0,0 +1,36 @@ +import { useWatch } from 'react-hook-form'; +import { + EModelEndpoint, + mergeFileConfig, + resolveEndpointType, + getEndpointFileConfig, +} from 'librechat-data-provider'; +import type { EndpointFileConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import { useGetFileConfig, useGetEndpointsQuery } from '~/data-provider'; + +export default function useAgentFileConfig(): { + endpointType: EModelEndpoint | string | undefined; + providerValue: string | undefined; + endpointFileConfig: EndpointFileConfig; +} { + const providerOption = useWatch({ name: 'provider' }); + const { data: endpointsConfig } = useGetEndpointsQuery(); + const { data: fileConfig = null } = useGetFileConfig({ + select: (data) => mergeFileConfig(data), + }); + + const providerValue = + typeof providerOption === 'string' + ? providerOption + : (providerOption as { value?: string } | undefined)?.value; + + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents, providerValue); + const endpointFileConfig = getEndpointFileConfig({ + fileConfig, + endpointType, + endpoint: providerValue || EModelEndpoint.agents, + }); + + return { endpointType, providerValue, endpointFileConfig }; +} diff --git a/client/src/hooks/Files/useDragHelpers.ts b/client/src/hooks/Files/useDragHelpers.ts index f931da408c..7c6c3bd155 100644 --- a/client/src/hooks/Files/useDragHelpers.ts +++ b/client/src/hooks/Files/useDragHelpers.ts @@ -13,6 +13,7 @@ import { EModelEndpoint, mergeFileConfig, AgentCapabilities, + resolveEndpointType, isAssistantsEndpoint, getEndpointFileConfig, defaultAgentCapabilities, @@ -69,7 +70,19 @@ export default function useDragHelpers() { (item: { files: File[] }) => { /** Early block: leverage endpoint file config to prevent drag/drop on disabled endpoints */ const currentEndpoint = conversationRef.current?.endpoint ?? 'default'; - const currentEndpointType = conversationRef.current?.endpointType ?? undefined; + const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); + + /** Get agent data from cache; if absent, provider-specific file config restrictions are bypassed client-side */ + const agentId = conversationRef.current?.agent_id; + const agent = agentId + ? queryClient.getQueryData([QueryKeys.agent, agentId]) + : undefined; + + const currentEndpointType = resolveEndpointType( + endpointsConfig, + currentEndpoint, + agent?.provider, + ); const cfg = queryClient.getQueryData([QueryKeys.fileConfig]); if (cfg) { const mergedCfg = mergeFileConfig(cfg); @@ -92,27 +105,21 @@ export default function useDragHelpers() { return; } - const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); const agentsConfig = endpointsConfig?.[EModelEndpoint.agents]; const capabilities = agentsConfig?.capabilities ?? defaultAgentCapabilities; const fileSearchEnabled = capabilities.includes(AgentCapabilities.file_search) === true; const codeEnabled = capabilities.includes(AgentCapabilities.execute_code) === true; const contextEnabled = capabilities.includes(AgentCapabilities.context) === true; - /** Get agent permissions at drop time */ - const agentId = conversationRef.current?.agent_id; let fileSearchAllowedByAgent = true; let codeAllowedByAgent = true; if (agentId && !isEphemeralAgent(agentId)) { - /** Agent data from cache */ - const agent = queryClient.getQueryData([QueryKeys.agent, agentId]); if (agent) { const agentTools = agent.tools as string[] | undefined; fileSearchAllowedByAgent = agentTools?.includes(Tools.file_search) ?? false; codeAllowedByAgent = agentTools?.includes(Tools.execute_code) ?? false; } else { - /** If agent exists but not found, disallow */ fileSearchAllowedByAgent = false; codeAllowedByAgent = false; } diff --git a/client/src/hooks/Files/useFileHandling.ts b/client/src/hooks/Files/useFileHandling.ts index be62700651..635937a6fa 100644 --- a/client/src/hooks/Files/useFileHandling.ts +++ b/client/src/hooks/Files/useFileHandling.ts @@ -30,8 +30,10 @@ type UseFileHandling = { fileSetter?: FileSetter; fileFilter?: (file: File) => boolean; additionalMetadata?: Record; - /** Overrides both `endpoint` and `endpointType` for validation and upload routing */ - endpointOverride?: EModelEndpoint; + /** Overrides `endpoint` for upload routing; also used as `endpointType` fallback when `endpointTypeOverride` is not set */ + endpointOverride?: EModelEndpoint | string; + /** Overrides `endpointType` independently from `endpointOverride` */ + endpointTypeOverride?: EModelEndpoint | string; }; export type FileHandlingState = { @@ -64,9 +66,10 @@ const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: Fil const agent_id = params?.additionalMetadata?.agent_id ?? ''; const assistant_id = params?.additionalMetadata?.assistant_id ?? ''; const endpointOverride = params?.endpointOverride; + const endpointTypeOverride = params?.endpointTypeOverride; const endpointType = useMemo( - () => endpointOverride ?? conversation?.endpointType, - [endpointOverride, conversation?.endpointType], + () => endpointTypeOverride ?? endpointOverride ?? conversation?.endpointType, + [endpointTypeOverride, endpointOverride, conversation?.endpointType], ); const endpoint = useMemo( () => endpointOverride ?? conversation?.endpoint ?? 'default', diff --git a/client/src/hooks/Files/useSharePointFileHandling.ts b/client/src/hooks/Files/useSharePointFileHandling.ts index a398bd594b..a04ef0104b 100644 --- a/client/src/hooks/Files/useSharePointFileHandling.ts +++ b/client/src/hooks/Files/useSharePointFileHandling.ts @@ -10,7 +10,8 @@ interface UseSharePointFileHandlingProps { toolResource?: string; fileFilter?: (file: File) => boolean; additionalMetadata?: Record; - endpointOverride?: EModelEndpoint; + endpointOverride?: EModelEndpoint | string; + endpointTypeOverride?: EModelEndpoint | string; } interface UseSharePointFileHandlingReturn { diff --git a/packages/data-provider/src/config.spec.ts b/packages/data-provider/src/config.spec.ts new file mode 100644 index 0000000000..4197cb754e --- /dev/null +++ b/packages/data-provider/src/config.spec.ts @@ -0,0 +1,315 @@ +import type { TEndpointsConfig } from './types'; +import { EModelEndpoint, isDocumentSupportedProvider } from './schemas'; +import { getEndpointFileConfig, mergeFileConfig } from './file-config'; +import { resolveEndpointType } from './config'; + +const endpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { userProvide: false, order: 0 }, + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + [EModelEndpoint.anthropic]: { userProvide: false, order: 6 }, + [EModelEndpoint.bedrock]: { userProvide: false, order: 7 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, + 'Some Endpoint': { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, + Gemini: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +describe('resolveEndpointType', () => { + describe('non-agents endpoints', () => { + it('returns the config type for a custom endpoint', () => { + expect(resolveEndpointType(endpointsConfig, 'Moonshot')).toBe(EModelEndpoint.custom); + }); + + it('returns the config type for a custom endpoint with spaces', () => { + expect(resolveEndpointType(endpointsConfig, 'Some Endpoint')).toBe(EModelEndpoint.custom); + }); + + it('returns the endpoint itself for a standard endpoint without a type field', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.openAI)).toBe( + EModelEndpoint.openAI, + ); + }); + + it('returns the endpoint itself for anthropic', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.anthropic)).toBe( + EModelEndpoint.anthropic, + ); + }); + + it('ignores agentProvider when endpoint is not agents', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.openAI, 'Moonshot')).toBe( + EModelEndpoint.openAI, + ); + }); + }); + + describe('agents endpoint with provider', () => { + it('resolves to custom for a custom agent provider', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'Moonshot')).toBe( + EModelEndpoint.custom, + ); + }); + + it('resolves to custom for a custom agent provider with spaces', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'Some Endpoint')).toBe( + EModelEndpoint.custom, + ); + }); + + it('returns the provider itself for a standard agent provider (no type field)', () => { + expect( + resolveEndpointType(endpointsConfig, EModelEndpoint.agents, EModelEndpoint.openAI), + ).toBe(EModelEndpoint.openAI); + }); + + it('returns bedrock for a bedrock agent provider', () => { + expect( + resolveEndpointType(endpointsConfig, EModelEndpoint.agents, EModelEndpoint.bedrock), + ).toBe(EModelEndpoint.bedrock); + }); + + it('returns the provider name when provider is not in endpointsConfig', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'UnknownProvider')).toBe( + 'UnknownProvider', + ); + }); + }); + + describe('agents endpoint without provider', () => { + it('falls back to agents when no provider', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.agents)).toBe( + EModelEndpoint.agents, + ); + }); + + it('falls back to agents when provider is null', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.agents, null)).toBe( + EModelEndpoint.agents, + ); + }); + + it('falls back to agents when provider is undefined', () => { + expect(resolveEndpointType(endpointsConfig, EModelEndpoint.agents, undefined)).toBe( + EModelEndpoint.agents, + ); + }); + }); + + describe('edge cases', () => { + it('returns undefined for null endpoint', () => { + expect(resolveEndpointType(endpointsConfig, null)).toBeUndefined(); + }); + + it('returns undefined for undefined endpoint', () => { + expect(resolveEndpointType(endpointsConfig, undefined)).toBeUndefined(); + }); + + it('handles null endpointsConfig', () => { + expect(resolveEndpointType(null, EModelEndpoint.agents, 'Moonshot')).toBe('Moonshot'); + }); + + it('handles undefined endpointsConfig', () => { + expect(resolveEndpointType(undefined, 'Moonshot')).toBe('Moonshot'); + }); + }); +}); + +describe('resolveEndpointType + getEndpointFileConfig integration', () => { + const fileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { fileLimit: 5 }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + + it('agent with Moonshot provider uses Moonshot-specific config', () => { + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'Moonshot'); + const config = getEndpointFileConfig({ + fileConfig, + endpointType, + endpoint: 'Moonshot', + }); + expect(config.fileLimit).toBe(5); + }); + + it('agent with provider not in fileConfig falls back through custom → agents', () => { + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'Gemini'); + const config = getEndpointFileConfig({ + fileConfig, + endpointType, + endpoint: 'Gemini', + }); + expect(config.fileLimit).toBe(20); + }); + + it('agent without provider falls back to agents config', () => { + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents); + const config = getEndpointFileConfig({ + fileConfig, + endpointType, + endpoint: EModelEndpoint.agents, + }); + expect(config.fileLimit).toBe(20); + }); + + it('custom fallback is used when present and provider has no specific config', () => { + const fileConfigWithCustom = mergeFileConfig({ + endpoints: { + custom: { fileLimit: 15 }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'Gemini'); + const config = getEndpointFileConfig({ + fileConfig: fileConfigWithCustom, + endpointType, + endpoint: 'Gemini', + }); + expect(config.fileLimit).toBe(15); + }); + + it('non-agents custom endpoint uses its specific config directly', () => { + const endpointType = resolveEndpointType(endpointsConfig, 'Moonshot'); + const config = getEndpointFileConfig({ + fileConfig, + endpointType, + endpoint: 'Moonshot', + }); + expect(config.fileLimit).toBe(5); + }); + + it('non-agents standard endpoint falls back to default when no specific config', () => { + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.openAI); + const config = getEndpointFileConfig({ + fileConfig, + endpointType, + endpoint: EModelEndpoint.openAI, + }); + expect(config.fileLimit).toBe(10); + }); +}); + +describe('resolveEndpointType + isDocumentSupportedProvider (upload menu)', () => { + it('agent with custom provider shows "Upload to Provider" (custom is document-supported)', () => { + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'Moonshot'); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it('agent with custom provider with spaces shows "Upload to Provider"', () => { + const endpointType = resolveEndpointType( + endpointsConfig, + EModelEndpoint.agents, + 'Some Endpoint', + ); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it('agent without provider falls back to agents (not document-supported)', () => { + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents); + expect(isDocumentSupportedProvider(endpointType)).toBe(false); + }); + + it('agent with openAI provider is document-supported', () => { + const endpointType = resolveEndpointType( + endpointsConfig, + EModelEndpoint.agents, + EModelEndpoint.openAI, + ); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it('agent with anthropic provider is document-supported', () => { + const endpointType = resolveEndpointType( + endpointsConfig, + EModelEndpoint.agents, + EModelEndpoint.anthropic, + ); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it('agent with bedrock provider is document-supported', () => { + const endpointType = resolveEndpointType( + endpointsConfig, + EModelEndpoint.agents, + EModelEndpoint.bedrock, + ); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it('direct custom endpoint (not agents) is document-supported', () => { + const endpointType = resolveEndpointType(endpointsConfig, 'Moonshot'); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it('direct standard endpoint is document-supported', () => { + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.openAI); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it('agent with unknown provider not in endpointsConfig is not document-supported', () => { + const endpointType = resolveEndpointType( + endpointsConfig, + EModelEndpoint.agents, + 'UnknownProvider', + ); + expect(isDocumentSupportedProvider(endpointType)).toBe(false); + }); + + it('same custom endpoint shows same result whether used directly or through agents', () => { + const directType = resolveEndpointType(endpointsConfig, 'Moonshot'); + const agentType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents, 'Moonshot'); + expect(isDocumentSupportedProvider(directType)).toBe(isDocumentSupportedProvider(agentType)); + }); +}); + +describe('any custom endpoint is document-supported regardless of name', () => { + const arbitraryNames = [ + 'My LLM Gateway', + 'company-internal-api', + 'LiteLLM Proxy', + 'test_endpoint_123', + 'AI Studio', + 'ACME Corp', + 'localhost:8080', + ]; + + const configWithArbitraryEndpoints: TEndpointsConfig = { + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + ...Object.fromEntries( + arbitraryNames.map((name) => [ + name, + { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, + ]), + ), + }; + + it.each(arbitraryNames)('direct custom endpoint "%s" is document-supported', (name) => { + const endpointType = resolveEndpointType(configWithArbitraryEndpoints, name); + expect(endpointType).toBe(EModelEndpoint.custom); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it.each(arbitraryNames)('agent with custom provider "%s" is document-supported', (name) => { + const endpointType = resolveEndpointType( + configWithArbitraryEndpoints, + EModelEndpoint.agents, + name, + ); + expect(endpointType).toBe(EModelEndpoint.custom); + expect(isDocumentSupportedProvider(endpointType)).toBe(true); + }); + + it.each(arbitraryNames)( + '"%s" resolves the same whether used directly or through an agent', + (name) => { + const directType = resolveEndpointType(configWithArbitraryEndpoints, name); + const agentType = resolveEndpointType( + configWithArbitraryEndpoints, + EModelEndpoint.agents, + name, + ); + expect(directType).toBe(agentType); + }, + ); +}); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 6a77508f59..61be4b2116 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import type { ZodError } from 'zod'; import type { TEndpointsConfig, TModelsConfig, TConfig } from './types'; -import { EModelEndpoint, eModelEndpointSchema } from './schemas'; +import { EModelEndpoint, eModelEndpointSchema, isAgentsEndpoint } from './schemas'; import { specsConfigSchema, TSpecsConfig } from './models'; import { fileConfigSchema } from './file-config'; import { apiBaseUrl } from './api-endpoints'; @@ -1926,6 +1926,38 @@ export function getEndpointField< return config[property]; } +/** + * Resolves the effective endpoint type: + * - Non-agents endpoint: config.type || endpoint + * - Agents + provider: config[provider].type || provider + * - Agents, no provider: EModelEndpoint.agents + * + * Returns `undefined` when endpoint is null/undefined. + */ +export function resolveEndpointType( + endpointsConfig: TEndpointsConfig | undefined | null, + endpoint: string | null | undefined, + agentProvider?: string | null, +): EModelEndpoint | string | undefined { + if (!endpoint) { + return undefined; + } + + if (!isAgentsEndpoint(endpoint)) { + return getEndpointField(endpointsConfig, endpoint, 'type') || endpoint; + } + + if (agentProvider) { + const providerType = getEndpointField(endpointsConfig, agentProvider, 'type'); + if (providerType) { + return providerType; + } + return agentProvider; + } + + return EModelEndpoint.agents; +} + /** Resolves the `defaultParamsEndpoint` for a given endpoint from its custom params config */ export function getDefaultParamsEndpoint( endpointsConfig: TEndpointsConfig | undefined | null, From 4a8a5b5994531ec2e2e4929f35e446a9be8da208 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 7 Mar 2026 20:13:52 -0500 Subject: [PATCH 008/111] =?UTF-8?q?=F0=9F=94=92=20fix:=20Hex-normalized=20?= =?UTF-8?q?IPv4-mapped=20IPv6=20in=20Domain=20Validation=20(#12130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 fix: handle hex-normalized IPv4-mapped IPv6 in domain validation * fix: Enhance IPv6 private address detection in domain validation - Added tests for detecting IPv4-compatible, 6to4, NAT64, and Teredo addresses. - Implemented `extractEmbeddedIPv4` function to identify private IPv4 addresses within various IPv6 formats. - Updated `isPrivateIP` function to utilize the new extraction logic for improved accuracy in address validation. * fix: Update private IPv4 detection logic in domain validation - Enhanced the `isPrivateIPv4` function to accurately identify additional private and non-routable IPv4 ranges. - Adjusted the return logic in `resolveHostnameSSRF` to utilize the updated private IP detection for improved hostname validation. * test: Expand private IP detection tests in domain validation - Added tests for additional private IPv4 ranges including 0.0.0.0/8, 100.64.0.0/10, 192.0.0.0/24, and 198.18.0.0/15. - Updated existing tests to ensure accurate detection of private and multicast IP addresses in the `isPrivateIP` function. - Enhanced `resolveHostnameSSRF` to correctly identify private literal IPv4 addresses without DNS lookup. * refactor: Rename and enhance embedded IPv4 detection in IPv6 addresses - Renamed `extractEmbeddedIPv4` to `hasPrivateEmbeddedIPv4` for clarity on its purpose. - Updated logic to accurately check for private IPv4 addresses embedded in Teredo, 6to4, and NAT64 IPv6 formats. - Improved the `isPrivateIP` function to utilize the new naming and logic for better readability and accuracy. - Enhanced documentation for clarity on the functionality of the updated methods. * feat: Enhance private IPv4 detection in embedded IPv6 addresses - Added additional checks in `hasPrivateEmbeddedIPv4` to ensure only valid private IPv4 formats are recognized. - Improved the logic for identifying private IPv4 addresses embedded within various IPv6 formats, enhancing overall accuracy. * test: Add additional test for hostname resolution in SSRF detection - Included a new test case in `resolveHostnameSSRF` to validate the detection of private IPv4 addresses embedded in IPv6 formats for the hostname 'meta.example.com'. - Enhanced existing tests to ensure comprehensive coverage of hostname resolution scenarios. * fix: Set redirect option to 'manual' in undiciFetch calls - Updated undiciFetch calls in MCPConnection to include the redirect option set to 'manual' for better control over HTTP redirects. - Added documentation comments regarding SSRF pre-checks for WebSocket connections, highlighting the limitations of the current SDK regarding DNS resolution. * test: Add integration tests for MCP SSRF protections - Introduced a new test suite for MCP SSRF protections, verifying that MCPConnection does not follow HTTP redirects to private IPs and blocks WebSocket connections to private IPs when SSRF protection is enabled. - Implemented tests to ensure correct behavior of the connection under various scenarios, including redirect handling and WebSocket DNS resolution. * refactor: Improve SSRF protection logic for WebSocket connections - Enhanced the SSRF pre-check for WebSocket connections to validate resolved IPs, ensuring that allowlisting a domain does not grant trust to its resolved IPs at runtime. - Updated documentation comments to clarify the limitations of the current SDK regarding DNS resolution and the implications for SSRF protection. * test: Enhance MCP SSRF protection tests for redirect handling and WebSocket connections - Updated tests to ensure that MCPConnection does not follow HTTP redirects to private IPs, regardless of SSRF protection settings. - Added checks to verify that WebSocket connections to hosts resolving to private IPs are blocked, even when SSRF protection is disabled. - Improved documentation comments for clarity on the behavior of the tests and the implications for SSRF protection. * test: Refactor MCP SSRF protection test for WebSocket connection errors - Updated the test to use `await expect(...).rejects.not.toThrow(...)` for better readability and clarity. - Simplified the error handling logic while ensuring that SSRF rejections are correctly validated during connection failures. --- packages/api/src/auth/domain.spec.ts | 253 +++++++++++++++- packages/api/src/auth/domain.ts | 100 +++++-- .../mcp/__tests__/MCPConnectionSSRF.test.ts | 277 ++++++++++++++++++ packages/api/src/mcp/connection.ts | 30 +- 4 files changed, 627 insertions(+), 33 deletions(-) create mode 100644 packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts diff --git a/packages/api/src/auth/domain.spec.ts b/packages/api/src/auth/domain.spec.ts index 5f6187c9b4..9812960cd9 100644 --- a/packages/api/src/auth/domain.spec.ts +++ b/packages/api/src/auth/domain.spec.ts @@ -153,8 +153,9 @@ describe('isSSRFTarget', () => { expect(isSSRFTarget('169.254.0.1')).toBe(true); }); - it('should block 0.0.0.0', () => { + it('should block 0.0.0.0/8 (current network)', () => { expect(isSSRFTarget('0.0.0.0')).toBe(true); + expect(isSSRFTarget('0.1.2.3')).toBe(true); }); it('should allow public IPs', () => { @@ -230,8 +231,36 @@ describe('isPrivateIP', () => { expect(isPrivateIP('169.254.0.1')).toBe(true); }); - it('should detect 0.0.0.0', () => { + it('should detect 0.0.0.0/8 (current network)', () => { expect(isPrivateIP('0.0.0.0')).toBe(true); + expect(isPrivateIP('0.1.2.3')).toBe(true); + }); + + it('should detect 100.64.0.0/10 (CGNAT / shared address space)', () => { + expect(isPrivateIP('100.64.0.1')).toBe(true); + expect(isPrivateIP('100.127.255.255')).toBe(true); + expect(isPrivateIP('100.63.255.255')).toBe(false); + expect(isPrivateIP('100.128.0.1')).toBe(false); + }); + + it('should detect 192.0.0.0/24 (IETF protocol assignments)', () => { + expect(isPrivateIP('192.0.0.1')).toBe(true); + expect(isPrivateIP('192.0.0.255')).toBe(true); + expect(isPrivateIP('192.0.1.1')).toBe(false); + }); + + it('should detect 198.18.0.0/15 (benchmarking)', () => { + expect(isPrivateIP('198.18.0.1')).toBe(true); + expect(isPrivateIP('198.19.255.255')).toBe(true); + expect(isPrivateIP('198.17.0.1')).toBe(false); + expect(isPrivateIP('198.20.0.1')).toBe(false); + }); + + it('should detect 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved)', () => { + expect(isPrivateIP('224.0.0.1')).toBe(true); + expect(isPrivateIP('239.255.255.255')).toBe(true); + expect(isPrivateIP('240.0.0.1')).toBe(true); + expect(isPrivateIP('255.255.255.255')).toBe(true); }); it('should allow public IPs', () => { @@ -270,6 +299,144 @@ describe('isPrivateIP', () => { }); }); +describe('isPrivateIP - IPv4-mapped IPv6 hex-normalized form (CVE-style SSRF bypass)', () => { + /** + * Node.js URL parser normalizes IPv4-mapped IPv6 from dotted-decimal to hex: + * new URL('http://[::ffff:169.254.169.254]/').hostname → '::ffff:a9fe:a9fe' + * + * These tests confirm whether isPrivateIP catches the hex form that actually + * reaches it in production (via parseDomainSpec → new URL → hostname). + */ + it('should detect hex-normalized AWS metadata address (::ffff:a9fe:a9fe)', () => { + // ::ffff:169.254.169.254 → hex form after URL parsing + expect(isPrivateIP('::ffff:a9fe:a9fe')).toBe(true); + }); + + it('should detect hex-normalized loopback (::ffff:7f00:1)', () => { + // ::ffff:127.0.0.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:7f00:1')).toBe(true); + }); + + it('should detect hex-normalized 192.168.x.x (::ffff:c0a8:101)', () => { + // ::ffff:192.168.1.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:c0a8:101')).toBe(true); + }); + + it('should detect hex-normalized 10.x.x.x (::ffff:a00:1)', () => { + // ::ffff:10.0.0.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:a00:1')).toBe(true); + }); + + it('should detect hex-normalized 172.16.x.x (::ffff:ac10:1)', () => { + // ::ffff:172.16.0.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:ac10:1')).toBe(true); + }); + + it('should detect hex-normalized 0.0.0.0 (::ffff:0:0)', () => { + // ::ffff:0.0.0.0 → hex form after URL parsing + expect(isPrivateIP('::ffff:0:0')).toBe(true); + }); + + it('should allow hex-normalized public IPs (::ffff:808:808 = 8.8.8.8)', () => { + expect(isPrivateIP('::ffff:808:808')).toBe(false); + }); + + it('should detect IPv4-compatible addresses without ffff prefix (::XXXX:XXXX)', () => { + expect(isPrivateIP('::7f00:1')).toBe(true); + expect(isPrivateIP('::a9fe:a9fe')).toBe(true); + expect(isPrivateIP('::c0a8:101')).toBe(true); + expect(isPrivateIP('::a00:1')).toBe(true); + }); + + it('should allow public IPs in IPv4-compatible form', () => { + expect(isPrivateIP('::808:808')).toBe(false); + }); + + it('should detect 6to4 addresses embedding private IPv4 (2002:XXXX:XXXX::)', () => { + expect(isPrivateIP('2002:7f00:1::')).toBe(true); + expect(isPrivateIP('2002:a9fe:a9fe::')).toBe(true); + expect(isPrivateIP('2002:c0a8:101::')).toBe(true); + expect(isPrivateIP('2002:a00:1::')).toBe(true); + }); + + it('should allow 6to4 addresses embedding public IPv4', () => { + expect(isPrivateIP('2002:808:808::')).toBe(false); + }); + + it('should detect NAT64 addresses embedding private IPv4 (64:ff9b::XXXX:XXXX)', () => { + expect(isPrivateIP('64:ff9b::7f00:1')).toBe(true); + expect(isPrivateIP('64:ff9b::a9fe:a9fe')).toBe(true); + }); + + it('should detect Teredo addresses with complement-encoded private IPv4 (RFC 4380)', () => { + // Teredo stores external IPv4 as bitwise complement in last 32 bits + // 127.0.0.1 → complement: 0x80ff:0xfffe + expect(isPrivateIP('2001::80ff:fffe')).toBe(true); + // 169.254.169.254 → complement: 0x5601:0x5601 + expect(isPrivateIP('2001::5601:5601')).toBe(true); + // 10.0.0.1 → complement: 0xf5ff:0xfffe + expect(isPrivateIP('2001::f5ff:fffe')).toBe(true); + }); + + it('should allow Teredo addresses with complement-encoded public IPv4', () => { + // 8.8.8.8 → complement: 0xf7f7:0xf7f7 + expect(isPrivateIP('2001::f7f7:f7f7')).toBe(false); + }); + + it('should confirm URL parser produces the hex form that bypasses dotted regex', () => { + // This test documents the exact normalization gap + const hostname = new URL('http://[::ffff:169.254.169.254]/').hostname.replace(/^\[|\]$/g, ''); + expect(hostname).toBe('::ffff:a9fe:a9fe'); // hex, not dotted + // The hostname that actually reaches isPrivateIP must be caught + expect(isPrivateIP(hostname)).toBe(true); + }); +}); + +describe('isActionDomainAllowed - IPv4-mapped IPv6 hex SSRF bypass (end-to-end)', () => { + beforeEach(() => { + mockedLookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }] as never); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should block http://[::ffff:169.254.169.254]/ (AWS metadata via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:169.254.169.254]/', null)).toBe(false); + }); + + it('should block http://[::ffff:127.0.0.1]/ (loopback via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:127.0.0.1]/', null)).toBe(false); + }); + + it('should block http://[::ffff:192.168.1.1]/ (private via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:192.168.1.1]/', null)).toBe(false); + }); + + it('should block http://[::ffff:10.0.0.1]/ (private via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:10.0.0.1]/', null)).toBe(false); + }); + + it('should allow http://[::ffff:8.8.8.8]/ (public via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:8.8.8.8]/', null)).toBe(true); + }); + + it('should block IPv4-compatible IPv6 without ffff prefix', async () => { + expect(await isActionDomainAllowed('http://[::127.0.0.1]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[::169.254.169.254]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[0:0:0:0:0:0:127.0.0.1]/', null)).toBe(false); + }); + + it('should block 6to4 addresses embedding private IPv4', async () => { + expect(await isActionDomainAllowed('http://[2002:7f00:1::]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[2002:a9fe:a9fe::]/', null)).toBe(false); + }); + + it('should block NAT64 addresses embedding private IPv4', async () => { + expect(await isActionDomainAllowed('http://[64:ff9b::127.0.0.1]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[64:ff9b::169.254.169.254]/', null)).toBe(false); + }); +}); + describe('resolveHostnameSSRF', () => { afterEach(() => { jest.clearAllMocks(); @@ -298,16 +465,50 @@ describe('resolveHostnameSSRF', () => { expect(await resolveHostnameSSRF('example.com')).toBe(false); }); - it('should skip literal IPv4 addresses (handled by isSSRFTarget)', async () => { - expect(await resolveHostnameSSRF('169.254.169.254')).toBe(false); + it('should detect private literal IPv4 addresses without DNS lookup', async () => { + expect(await resolveHostnameSSRF('169.254.169.254')).toBe(true); + expect(await resolveHostnameSSRF('127.0.0.1')).toBe(true); + expect(await resolveHostnameSSRF('10.0.0.1')).toBe(true); expect(mockedLookup).not.toHaveBeenCalled(); }); - it('should skip literal IPv6 addresses', async () => { - expect(await resolveHostnameSSRF('::1')).toBe(false); + it('should allow public literal IPv4 addresses without DNS lookup', async () => { + expect(await resolveHostnameSSRF('8.8.8.8')).toBe(false); + expect(await resolveHostnameSSRF('93.184.216.34')).toBe(false); expect(mockedLookup).not.toHaveBeenCalled(); }); + it('should detect private IPv6 literals without DNS lookup', async () => { + expect(await resolveHostnameSSRF('::1')).toBe(true); + expect(await resolveHostnameSSRF('fc00::1')).toBe(true); + expect(await resolveHostnameSSRF('fe80::1')).toBe(true); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should detect hex-normalized IPv4-mapped IPv6 literals', async () => { + expect(await resolveHostnameSSRF('::ffff:a9fe:a9fe')).toBe(true); + expect(await resolveHostnameSSRF('::ffff:7f00:1')).toBe(true); + expect(await resolveHostnameSSRF('[::ffff:a9fe:a9fe]')).toBe(true); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should allow public IPv6 literals without DNS lookup', async () => { + expect(await resolveHostnameSSRF('2001:db8::1')).toBe(false); + expect(await resolveHostnameSSRF('::ffff:808:808')).toBe(false); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should detect private IPv6 addresses returned from DNS lookup', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '::1', family: 6 }] as never); + expect(await resolveHostnameSSRF('ipv6-loopback.example.com')).toBe(true); + + mockedLookup.mockResolvedValueOnce([{ address: 'fc00::1', family: 6 }] as never); + expect(await resolveHostnameSSRF('ula.example.com')).toBe(true); + + mockedLookup.mockResolvedValueOnce([{ address: '::ffff:a9fe:a9fe', family: 6 }] as never); + expect(await resolveHostnameSSRF('meta.example.com')).toBe(true); + }); + it('should fail open on DNS resolution failure', async () => { mockedLookup.mockRejectedValueOnce(new Error('ENOTFOUND')); expect(await resolveHostnameSSRF('nonexistent.example.com')).toBe(false); @@ -915,4 +1116,44 @@ describe('isMCPDomainAllowed', () => { expect(await isMCPDomainAllowed({ url: 'wss://example.com' }, ['example.com'])).toBe(true); }); }); + + describe('IPv4-mapped IPv6 hex SSRF bypass', () => { + it('should block MCP server targeting AWS metadata via IPv6-mapped address', async () => { + const config = { url: 'http://[::ffff:169.254.169.254]/mcp' }; + expect(await isMCPDomainAllowed(config, null)).toBe(false); + }); + + it('should block MCP server targeting loopback via IPv6-mapped address', async () => { + const config = { url: 'http://[::ffff:127.0.0.1]/mcp' }; + expect(await isMCPDomainAllowed(config, null)).toBe(false); + }); + + it('should block MCP server targeting private range via IPv6-mapped address', async () => { + expect(await isMCPDomainAllowed({ url: 'http://[::ffff:10.0.0.1]/mcp' }, null)).toBe(false); + expect(await isMCPDomainAllowed({ url: 'http://[::ffff:192.168.1.1]/mcp' }, null)).toBe( + false, + ); + }); + + it('should block WebSocket MCP targeting private range via IPv6-mapped address', async () => { + expect(await isMCPDomainAllowed({ url: 'ws://[::ffff:127.0.0.1]/mcp' }, null)).toBe(false); + expect(await isMCPDomainAllowed({ url: 'wss://[::ffff:10.0.0.1]/mcp' }, null)).toBe(false); + }); + + it('should allow MCP server targeting public IP via IPv6-mapped address', async () => { + const config = { url: 'http://[::ffff:8.8.8.8]/mcp' }; + expect(await isMCPDomainAllowed(config, null)).toBe(true); + }); + + it('should block MCP server targeting 6to4 embedded private IPv4', async () => { + expect(await isMCPDomainAllowed({ url: 'http://[2002:7f00:1::]/mcp' }, null)).toBe(false); + expect(await isMCPDomainAllowed({ url: 'ws://[2002:a9fe:a9fe::]/mcp' }, null)).toBe(false); + }); + + it('should block MCP server targeting NAT64 embedded private IPv4', async () => { + expect(await isMCPDomainAllowed({ url: 'http://[64:ff9b::127.0.0.1]/mcp' }, null)).toBe( + false, + ); + }); + }); }); diff --git a/packages/api/src/auth/domain.ts b/packages/api/src/auth/domain.ts index f2e86875d4..2761a80b55 100644 --- a/packages/api/src/auth/domain.ts +++ b/packages/api/src/auth/domain.ts @@ -24,26 +24,79 @@ export function isEmailDomainAllowed(email: string, allowedDomains?: string[] | return allowedDomains.some((allowedDomain) => allowedDomain?.toLowerCase() === domain); } -/** Checks if IPv4 octets fall within private, reserved, or link-local ranges */ +/** Checks if IPv4 octets fall within private, reserved, or non-routable ranges */ function isPrivateIPv4(a: number, b: number, c: number): boolean { - if (a === 127) { + if (a === 0) { return true; } if (a === 10) { return true; } + if (a === 127) { + return true; + } + if (a === 100 && b >= 64 && b <= 127) { + return true; + } + if (a === 169 && b === 254) { + return true; + } if (a === 172 && b >= 16 && b <= 31) { return true; } if (a === 192 && b === 168) { return true; } - if (a === 169 && b === 254) { + if (a === 192 && b === 0 && c === 0) { return true; } - if (a === 0 && b === 0 && c === 0) { + if (a === 198 && (b === 18 || b === 19)) { return true; } + if (a >= 224) { + return true; + } + return false; +} + +/** Checks if an IPv6 address embeds a private IPv4 via 6to4, NAT64, or Teredo */ +function hasPrivateEmbeddedIPv4(ipv6: string): boolean { + if (!ipv6.startsWith('2002:') && !ipv6.startsWith('64:ff9b::') && !ipv6.startsWith('2001::')) { + return false; + } + const segments = ipv6.split(':').filter((s) => s !== ''); + + if (ipv6.startsWith('2002:') && segments.length >= 3) { + const hi = parseInt(segments[1], 16); + const lo = parseInt(segments[2], 16); + if (!isNaN(hi) && !isNaN(lo)) { + return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff); + } + } + + if (ipv6.startsWith('64:ff9b::')) { + const lastTwo = segments.slice(-2); + if (lastTwo.length === 2) { + const hi = parseInt(lastTwo[0], 16); + const lo = parseInt(lastTwo[1], 16); + if (!isNaN(hi) && !isNaN(lo)) { + return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff); + } + } + } + + // RFC 4380: Teredo stores external IPv4 as bitwise complement in last 32 bits + if (ipv6.startsWith('2001::')) { + const lastTwo = segments.slice(-2); + if (lastTwo.length === 2) { + const hi = parseInt(lastTwo[0], 16); + const lo = parseInt(lastTwo[1], 16); + if (!isNaN(hi) && !isNaN(lo)) { + return isPrivateIPv4((~hi >> 8) & 0xff, ~hi & 0xff, (~lo >> 8) & 0xff); + } + } + } + return false; } @@ -52,7 +105,10 @@ function isPrivateIPv4(a: number, b: number, c: number): boolean { * Handles IPv4, IPv6, and IPv4-mapped IPv6 addresses (::ffff:A.B.C.D). */ export function isPrivateIP(ip: string): boolean { - const normalized = ip.toLowerCase().trim(); + const normalized = ip + .toLowerCase() + .trim() + .replace(/^\[|\]$/g, ''); const mappedMatch = normalized.match(/^::ffff:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); if (mappedMatch) { @@ -60,42 +116,52 @@ export function isPrivateIP(ip: string): boolean { return isPrivateIPv4(a, b, c); } + const hexMappedMatch = normalized.match(/^(?:::ffff:|::)([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); + if (hexMappedMatch) { + const hi = parseInt(hexMappedMatch[1], 16); + const lo = parseInt(hexMappedMatch[2], 16); + return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff); + } + const ipv4Match = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); if (ipv4Match) { const [, a, b, c] = ipv4Match.map(Number); return isPrivateIPv4(a, b, c); } - const ipv6 = normalized.replace(/^\[|\]$/g, ''); if ( - ipv6 === '::1' || - ipv6 === '::' || - ipv6.startsWith('fc') || - ipv6.startsWith('fd') || - ipv6.startsWith('fe80') + normalized === '::1' || + normalized === '::' || + normalized.startsWith('fc') || + normalized.startsWith('fd') || + normalized.startsWith('fe80') ) { return true; } + if (hasPrivateEmbeddedIPv4(normalized)) { + return true; + } + return false; } /** - * Resolves a hostname via DNS and checks if any resolved address is a private/reserved IP. - * Detects DNS-based SSRF bypasses (e.g., nip.io wildcard DNS, attacker-controlled nameservers). - * Fails open: returns false if DNS resolution fails, since hostname-only checks still apply - * and the actual HTTP request would also fail. + * Checks if a hostname resolves to a private/reserved IP address. + * Directly validates literal IPv4 and IPv6 addresses without DNS lookup. + * For hostnames, resolves via DNS and checks all returned addresses. + * Fails open on DNS errors (returns false), since the HTTP request would also fail. */ export async function resolveHostnameSSRF(hostname: string): Promise { const normalizedHost = hostname.toLowerCase().trim(); if (/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(normalizedHost)) { - return false; + return isPrivateIP(normalizedHost); } const ipv6Check = normalizedHost.replace(/^\[|\]$/g, ''); if (ipv6Check.includes(':')) { - return false; + return isPrivateIP(ipv6Check); } try { diff --git a/packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts new file mode 100644 index 0000000000..b4eb58dbef --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts @@ -0,0 +1,277 @@ +/** + * Integration tests for MCP SSRF protections. + * + * These tests spin up real in-process HTTP servers and verify that MCPConnection: + * + * 1. Does NOT follow HTTP redirects from SSE/StreamableHTTP transports + * (redirect: 'manual' prevents SSRF via server-controlled 301/302) + * 2. Blocks WebSocket connections to hosts that DNS-resolve to private IPs, + * regardless of whether useSSRFProtection is enabled (allowlist scenario) + */ + +import * as net from 'net'; +import * as http from 'http'; +import { randomUUID } from 'crypto'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { Socket } from 'net'; +import { MCPConnection } from '~/mcp/connection'; +import { resolveHostnameSSRF } from '~/auth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0 }, +})); + +const mockedResolveHostnameSSRF = resolveHostnameSSRF as jest.MockedFunction< + typeof resolveHostnameSSRF +>; + +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) { + return; + } + (conn as unknown as { shouldStopReconnecting: boolean }).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +interface TestServer { + url: string; + redirectHit: boolean; + close: () => Promise; +} + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +/** + * Creates an HTTP server that responds with a 301 redirect to a target URL. + * A second server is spun up at the redirect target to detect whether the + * redirect was actually followed. + */ +async function createRedirectingServer(redirectTarget: string): Promise { + const state = { redirectHit: false }; + + const targetPort = new URL(redirectTarget).port || '80'; + const targetServer = http.createServer((_req, res) => { + state.redirectHit = true; + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('You should not be here'); + }); + const destroyTargetSockets = trackSockets(targetServer); + await new Promise((resolve) => + targetServer.listen(parseInt(targetPort), '127.0.0.1', resolve), + ); + + const httpServer = http.createServer((_req, res) => { + res.writeHead(301, { Location: redirectTarget }); + res.end(); + }); + + const destroySockets = trackSockets(httpServer); + const port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + get redirectHit() { + return state.redirectHit; + }, + close: async () => { + await destroySockets(); + await destroyTargetSockets(); + }, + }; +} + +/** + * Creates a real StreamableHTTP MCP server for baseline connectivity tests. + */ +async function createStreamableServer(): Promise> { + const sessions = new Map(); + + const httpServer = http.createServer(async (req, res) => { + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); + const mcp = new McpServer({ name: 'test-ssrf', version: '0.0.1' }); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + const port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +describe('MCP SSRF protection – redirect blocking', () => { + let redirectServer: TestServer; + let conn: MCPConnection | null; + + afterEach(async () => { + await safeDisconnect(conn); + conn = null; + if (redirectServer) { + await redirectServer.close(); + } + jest.restoreAllMocks(); + }); + + it('should not follow redirects from streamable-http to a private IP', async () => { + const targetPort = await getFreePort(); + redirectServer = await createRedirectingServer( + `http://127.0.0.1:${targetPort}/latest/meta-data/`, + ); + + conn = new MCPConnection({ + serverName: 'redirect-test', + serverConfig: { type: 'streamable-http', url: redirectServer.url }, + useSSRFProtection: false, + }); + + await expect(conn.connect()).rejects.toThrow(); + expect(redirectServer.redirectHit).toBe(false); + }); + + it('should not follow redirects even with SSRF protection off (allowlist scenario)', async () => { + const targetPort = await getFreePort(); + redirectServer = await createRedirectingServer(`http://127.0.0.1:${targetPort}/admin`); + + conn = new MCPConnection({ + serverName: 'redirect-test-2', + serverConfig: { type: 'streamable-http', url: redirectServer.url }, + useSSRFProtection: false, + }); + + await expect(conn.connect()).rejects.toThrow(); + expect(redirectServer.redirectHit).toBe(false); + }); + + it('should connect normally to a non-redirecting streamable-http server', async () => { + const realServer = await createStreamableServer(); + try { + conn = new MCPConnection({ + serverName: 'legit-server', + serverConfig: { type: 'streamable-http', url: realServer.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + const tools = await conn.fetchTools(); + expect(tools).toBeDefined(); + } finally { + await safeDisconnect(conn); + conn = null; + await realServer.close(); + } + }); +}); + +describe('MCP SSRF protection – WebSocket DNS resolution', () => { + let conn: MCPConnection | null; + + afterEach(async () => { + await safeDisconnect(conn); + conn = null; + jest.restoreAllMocks(); + }); + + it('should block WebSocket to host resolving to private IP when SSRF protection is on', async () => { + mockedResolveHostnameSSRF.mockResolvedValueOnce(true); + + conn = new MCPConnection({ + serverName: 'ws-ssrf-test', + serverConfig: { type: 'websocket', url: 'ws://evil.example.com:8080/mcp' }, + useSSRFProtection: true, + }); + + await expect(conn.connect()).rejects.toThrow(/SSRF protection/); + expect(mockedResolveHostnameSSRF).toHaveBeenCalledWith( + expect.stringContaining('evil.example.com'), + ); + }); + + it('should block WebSocket to host resolving to private IP even with SSRF protection off', async () => { + mockedResolveHostnameSSRF.mockResolvedValueOnce(true); + + conn = new MCPConnection({ + serverName: 'ws-ssrf-allowlist', + serverConfig: { type: 'websocket', url: 'ws://allowlisted.example.com:8080/mcp' }, + useSSRFProtection: false, + }); + + await expect(conn.connect()).rejects.toThrow(/SSRF protection/); + expect(mockedResolveHostnameSSRF).toHaveBeenCalledWith( + expect.stringContaining('allowlisted.example.com'), + ); + }); + + it('should allow WebSocket to host resolving to public IP', async () => { + mockedResolveHostnameSSRF.mockResolvedValueOnce(false); + + conn = new MCPConnection({ + serverName: 'ws-public-test', + serverConfig: { type: 'websocket', url: 'ws://public.example.com:8080/mcp' }, + useSSRFProtection: true, + }); + + /** Fails on connect (no real server), but the error must not be an SSRF rejection. */ + await expect(conn.connect()).rejects.not.toThrow(/SSRF protection/); + }); +}); diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index 6e2633b758..83f1af1824 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -364,7 +364,7 @@ export class MCPConnection extends EventEmitter { const requestHeaders = getHeaders(); if (!requestHeaders) { - return undiciFetch(input, { ...init, dispatcher }); + return undiciFetch(input, { ...init, redirect: 'manual', dispatcher }); } let initHeaders: Record = {}; @@ -380,6 +380,7 @@ export class MCPConnection extends EventEmitter { return undiciFetch(input, { ...init, + redirect: 'manual', headers: { ...initHeaders, ...requestHeaders, @@ -425,21 +426,29 @@ export class MCPConnection extends EventEmitter { env: { ...getDefaultEnvironment(), ...(options.env ?? {}) }, }); - case 'websocket': + case 'websocket': { if (!isWebSocketOptions(options)) { throw new Error('Invalid options for websocket transport.'); } this.url = options.url; - if (this.useSSRFProtection) { - const wsHostname = new URL(options.url).hostname; - const isSSRF = await resolveHostnameSSRF(wsHostname); - if (isSSRF) { - throw new Error( - `SSRF protection: WebSocket host "${wsHostname}" resolved to a private/reserved IP address`, - ); - } + /** + * SSRF pre-check: always validate resolved IPs for WebSocket, regardless + * of allowlist configuration. Allowlisting a domain grants trust to that + * name, not to whatever IP it resolves to at runtime (DNS rebinding). + * + * Note: WebSocketClientTransport does its own DNS resolution, creating a + * small TOCTOU window. This is an SDK limitation — the transport accepts + * only a URL with no custom DNS lookup hook. + */ + const wsHostname = new URL(options.url).hostname; + const isSSRF = await resolveHostnameSSRF(wsHostname); + if (isSSRF) { + throw new Error( + `SSRF protection: WebSocket host "${wsHostname}" resolved to a private/reserved IP address`, + ); } return new WebSocketClientTransport(new URL(options.url)); + } case 'sse': { if (!isSSEOptions(options)) { @@ -486,6 +495,7 @@ export class MCPConnection extends EventEmitter { ); return undiciFetch(url, { ...init, + redirect: 'manual', dispatcher: sseAgent, headers: fetchHeaders, }); From 8b18a16446016a85b7c968aeb209bcbb976bdb5e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 8 Mar 2026 21:48:22 -0400 Subject: [PATCH 009/111] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20chore:=20Remove?= =?UTF-8?q?=20Docker=20Images=20by=20Named=20Tag=20in=20`deployed-update.j?= =?UTF-8?q?s`=20(#12138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: remove docker images by named tag instead of image ID * refactor: Simplify rebase logic and enhance error handling in deployed-update script - Removed unnecessary condition for rebasing, streamlining the update process. - Renamed variable for clarity when fetching Docker image tags. - Added error handling to catch and log failures during the update process, ensuring better visibility of issues. --- config/deployed-update.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/config/deployed-update.js b/config/deployed-update.js index 7b2a3bc594..7ce6eb106d 100644 --- a/config/deployed-update.js +++ b/config/deployed-update.js @@ -29,7 +29,7 @@ const shouldRebase = process.argv.includes('--rebase'); execSync('git checkout main', { stdio: 'inherit' }); console.purple('Pulling the latest code from main...'); execSync('git pull origin main', { stdio: 'inherit' }); - } else if (shouldRebase) { + } else { const currentBranch = getCurrentBranch(); console.purple(`Rebasing ${currentBranch} onto main...`); execSync('git rebase origin/main', { stdio: 'inherit' }); @@ -43,11 +43,14 @@ const shouldRebase = process.argv.includes('--rebase'); console.purple('Removing all tags for LibreChat `deployed` images...'); const repositories = ['registry.librechat.ai/danny-avila/librechat-dev-api', 'librechat-client']; repositories.forEach((repo) => { - const tags = execSync(`sudo docker images ${repo} -q`, { encoding: 'utf8' }) + const imageRefs = execSync(`sudo docker images ${repo} --format "{{.Repository}}:{{.Tag}}"`, { + encoding: 'utf8', + }) .split('\n') - .filter(Boolean); - tags.forEach((tag) => { - const removeImageCommand = `sudo docker rmi ${tag}`; + .filter(Boolean) + .filter((ref) => !ref.includes('')); + imageRefs.forEach((imageRef) => { + const removeImageCommand = `sudo docker rmi ${imageRef}`; console.orange(removeImageCommand); execSync(removeImageCommand, { stdio: 'inherit' }); }); @@ -58,11 +61,14 @@ const shouldRebase = process.argv.includes('--rebase'); console.orange(pullCommand); execSync(pullCommand, { stdio: 'inherit' }); - let startCommand = 'sudo docker compose -f ./deploy-compose.yml up -d'; + const startCommand = 'sudo docker compose -f ./deploy-compose.yml up -d'; console.green('Your LibreChat app is now up to date! Start the app with the following command:'); console.purple(startCommand); console.orange( "Note: it's also recommended to clear your browser cookies and localStorage for LibreChat to assure a fully clean installation.", ); console.orange("Also: Don't worry, your data is safe :)"); -})(); +})().catch((err) => { + console.error('Update script failed:', err.message); + process.exit(1); +}); From 32cadb1cc58040fa47e48d707ba1c4b23dfd6d06 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 8 Mar 2026 21:49:04 -0400 Subject: [PATCH 010/111] =?UTF-8?q?=F0=9F=A9=B9=20fix:=20MCP=20Server=20Re?= =?UTF-8?q?covery=20from=20Startup=20Inspection=20Failures=20(#12145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: MCP server reinitialization recovery mechanism - Added functionality to store a stub configuration for MCP servers that fail inspection at startup, allowing for recovery via reinitialization. - Introduced `reinspectServer` method in `MCPServersRegistry` to handle reinspection of previously failed servers. - Enhanced `MCPServersInitializer` to log and manage server initialization failures, ensuring proper handling of inspection failures. - Added integration tests to verify the recovery process for unreachable MCP servers, ensuring that stub configurations are stored and can be reinitialized successfully. - Updated type definitions to include `inspectionFailed` flag in server configurations for better state management. * fix: MCP server handling for inspection failures - Updated `reinitMCPServer` to return a structured response when the server is unreachable, providing clearer feedback on the failure. - Modified `ConnectionsRepository` to prevent connections to servers marked as inspection failed, improving error handling. - Adjusted `MCPServersRegistry` methods to ensure proper management of server states, including throwing errors for non-failed servers during reinspection. - Enhanced integration tests to validate the behavior of the system when dealing with unreachable MCP servers and inspection failures, ensuring robust recovery mechanisms. * fix: Clear all cached server configurations in MCPServersRegistry - Added a comment to clarify the necessity of clearing all cached server configurations when updating a server's configuration, as the cache is keyed by userId without a reverse index for enumeration. * fix: Update integration test for file_tools_server inspection handling - Modified the test to verify that the `file_tools_server` is stored as a stub when inspection fails, ensuring it can be reinitialized correctly. - Adjusted expectations to confirm that the `inspectionFailed` flag is set to true for the stub configuration, enhancing the robustness of the recovery mechanism. * test: Add unit tests for reinspecting servers in MCPServersRegistry - Introduced tests for the `reinspectServer` method to validate error handling when called on a healthy server and when the server does not exist. - Ensured that appropriate exceptions are thrown for both scenarios, enhancing the robustness of server state management. * test: Add integration test for concurrent reinspectServer calls - Introduced a new test to validate that multiple concurrent calls to reinspectServer do not crash or corrupt the server state. - Ensured that at least one call succeeds and any failures are due to the server not being in a failed state, enhancing the reliability of the reinitialization process. * test: Enhance integration test for concurrent MCP server reinitialization - Added a new test to validate that concurrent calls to reinitialize the MCP server do not crash or corrupt the server state. - Ensured that at least one call succeeds and that failures are handled gracefully, improving the reliability of the reinitialization process. - Reset MCPManager instance after each test to maintain a clean state for subsequent tests. --- api/server/services/Tools/mcp.js | 29 +- packages/api/src/mcp/ConnectionsRepository.ts | 3 + .../src/mcp/registry/MCPServersInitializer.ts | 22 +- .../src/mcp/registry/MCPServersRegistry.ts | 64 +++ .../MCPReinitRecovery.integration.test.ts | 488 ++++++++++++++++++ ...rversInitializer.cache_integration.spec.ts | 5 +- .../__tests__/MCPServersInitializer.test.ts | 11 +- .../__tests__/MCPServersRegistry.test.ts | 18 +- packages/api/src/mcp/types/index.ts | 2 + 9 files changed, 627 insertions(+), 15 deletions(-) create mode 100644 packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts diff --git a/api/server/services/Tools/mcp.js b/api/server/services/Tools/mcp.js index 10f2d71a18..7589043e10 100644 --- a/api/server/services/Tools/mcp.js +++ b/api/server/services/Tools/mcp.js @@ -1,8 +1,8 @@ const { logger } = require('@librechat/data-schemas'); const { CacheKeys, Constants } = require('librechat-data-provider'); +const { getMCPManager, getMCPServersRegistry, getFlowStateManager } = require('~/config'); const { findToken, createToken, updateToken, deleteTokens } = require('~/models'); const { updateMCPServerTools } = require('~/server/services/Config'); -const { getMCPManager, getFlowStateManager } = require('~/config'); const { getLogStores } = require('~/cache'); /** @@ -41,6 +41,33 @@ async function reinitMCPServer({ let oauthUrl = null; try { + const registry = getMCPServersRegistry(); + const serverConfig = await registry.getServerConfig(serverName, user?.id); + if (serverConfig?.inspectionFailed) { + logger.info( + `[MCP Reinitialize] Server ${serverName} had failed inspection, attempting reinspection`, + ); + try { + const storageLocation = serverConfig.dbId ? 'DB' : 'CACHE'; + await registry.reinspectServer(serverName, storageLocation, user?.id); + logger.info(`[MCP Reinitialize] Reinspection succeeded for server: ${serverName}`); + } catch (reinspectError) { + logger.error( + `[MCP Reinitialize] Reinspection failed for server ${serverName}:`, + reinspectError, + ); + return { + availableTools: null, + success: false, + message: `MCP server '${serverName}' is still unreachable`, + oauthRequired: false, + serverName, + oauthUrl: null, + tools: null, + }; + } + } + const customUserVars = userMCPAuthMap?.[`${Constants.mcp_prefix}${serverName}`]; const flowManager = _flowManager ?? getFlowStateManager(getLogStores(CacheKeys.FLOWS)); const mcpManager = getMCPManager(); diff --git a/packages/api/src/mcp/ConnectionsRepository.ts b/packages/api/src/mcp/ConnectionsRepository.ts index edd1eabc2e..e629934dda 100644 --- a/packages/api/src/mcp/ConnectionsRepository.ts +++ b/packages/api/src/mcp/ConnectionsRepository.ts @@ -140,6 +140,9 @@ export class ConnectionsRepository { } private isAllowedToConnectToServer(config: t.ParsedServerConfig) { + if (config.inspectionFailed) { + return false; + } //the repository is not allowed to be connected in case the Connection repository is shared (ownerId is undefined/null) and the server requires Auth or startup false. if (this.ownerId === undefined && (config.startup === false || config.requiresOAuth)) { return false; diff --git a/packages/api/src/mcp/registry/MCPServersInitializer.ts b/packages/api/src/mcp/registry/MCPServersInitializer.ts index 92505db12b..a8b8e3ca8a 100644 --- a/packages/api/src/mcp/registry/MCPServersInitializer.ts +++ b/packages/api/src/mcp/registry/MCPServersInitializer.ts @@ -1,11 +1,10 @@ -import { registryStatusCache as statusCache } from './cache/RegistryStatusCache'; -import { isLeader } from '~/cluster'; -import { withTimeout } from '~/utils'; import { logger } from '@librechat/data-schemas'; -import { ParsedServerConfig } from '~/mcp/types'; -import { sanitizeUrlForLogging } from '~/mcp/utils'; import type * as t from '~/mcp/types'; +import { registryStatusCache as statusCache } from './cache/RegistryStatusCache'; import { MCPServersRegistry } from './MCPServersRegistry'; +import { sanitizeUrlForLogging } from '~/mcp/utils'; +import { withTimeout } from '~/utils'; +import { isLeader } from '~/cluster'; const MCP_INIT_TIMEOUT_MS = process.env.MCP_INIT_TIMEOUT_MS != null ? parseInt(process.env.MCP_INIT_TIMEOUT_MS) : 30_000; @@ -80,11 +79,22 @@ export class MCPServersInitializer { MCPServersInitializer.logParsedConfig(serverName, result.config); } catch (error) { logger.error(`${MCPServersInitializer.prefix(serverName)} Failed to initialize:`, error); + try { + await MCPServersRegistry.getInstance().addServerStub(serverName, rawConfig, 'CACHE'); + logger.info( + `${MCPServersInitializer.prefix(serverName)} Stored stub config for recovery via reinitialize`, + ); + } catch (stubError) { + logger.error( + `${MCPServersInitializer.prefix(serverName)} Failed to store stub config:`, + stubError, + ); + } } } // Logs server configuration summary after initialization - private static logParsedConfig(serverName: string, config: ParsedServerConfig): void { + private static logParsedConfig(serverName: string, config: t.ParsedServerConfig): void { const prefix = MCPServersInitializer.prefix(serverName); logger.info(`${prefix} -------------------------------------------------┐`); logger.info(`${prefix} URL: ${config.url ? sanitizeUrlForLogging(config.url) : 'N/A'}`); diff --git a/packages/api/src/mcp/registry/MCPServersRegistry.ts b/packages/api/src/mcp/registry/MCPServersRegistry.ts index 0264a8ed7a..506f5b1baa 100644 --- a/packages/api/src/mcp/registry/MCPServersRegistry.ts +++ b/packages/api/src/mcp/registry/MCPServersRegistry.ts @@ -144,6 +144,24 @@ export class MCPServersRegistry { return result; } + /** + * Stores a minimal config stub so the server remains "known" to the registry + * even when inspection fails at startup. This enables reinitialize to recover. + */ + public async addServerStub( + serverName: string, + config: t.MCPOptions, + storageLocation: 'CACHE' | 'DB', + userId?: string, + ): Promise { + const configRepo = this.getConfigRepository(storageLocation); + const stubConfig: t.ParsedServerConfig = { ...config, inspectionFailed: true }; + const result = await configRepo.add(serverName, stubConfig, userId); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName, userId)); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName)); + return result; + } + public async addServer( serverName: string, config: t.MCPOptions, @@ -170,6 +188,52 @@ export class MCPServersRegistry { return await configRepo.add(serverName, parsedConfig, userId); } + /** + * Re-inspects a server that previously failed initialization. + * Uses the stored stub config to attempt a full inspection and replaces the stub on success. + */ + public async reinspectServer( + serverName: string, + storageLocation: 'CACHE' | 'DB', + userId?: string, + ): Promise { + const configRepo = this.getConfigRepository(storageLocation); + const existing = await configRepo.get(serverName, userId); + if (!existing) { + throw new Error(`Server "${serverName}" not found in ${storageLocation} for reinspection.`); + } + if (!existing.inspectionFailed) { + throw new Error( + `Server "${serverName}" is not in a failed state. Use updateServer() instead.`, + ); + } + + const { inspectionFailed: _, ...configForInspection } = existing; + let parsedConfig: t.ParsedServerConfig; + try { + parsedConfig = await MCPServerInspector.inspect( + serverName, + configForInspection, + undefined, + this.allowedDomains, + ); + } catch (error) { + logger.error(`[MCPServersRegistry] Reinspection failed for server "${serverName}":`, error); + if (isMCPDomainNotAllowedError(error)) { + throw error; + } + throw new MCPInspectionFailedError(serverName, error as Error); + } + + const updatedConfig = { ...parsedConfig, updatedAt: Date.now() }; + await configRepo.update(serverName, updatedConfig, userId); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName, userId)); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName)); + // Full clear required: getAllServerConfigs is keyed by userId with no reverse index to enumerate cached keys + await this.readThroughCacheAll.clear(); + return { serverName, config: updatedConfig }; + } + public async updateServer( serverName: string, config: t.MCPOptions, diff --git a/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts b/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts new file mode 100644 index 0000000000..b171e84d13 --- /dev/null +++ b/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts @@ -0,0 +1,488 @@ +/** + * Integration tests for MCP server reinitialize recovery (issue #12143). + * + * Reproduces the bug: when an MCP server is unreachable at startup, + * inspection fails and the server config is never stored — making the + * reinitialize button return 404 and blocking all recovery. + * + * These tests spin up a real in-process MCP server using the SDK's + * StreamableHTTPServerTransport and exercise the full + * MCPServersInitializer → MCPServersRegistry → MCPServerInspector pipeline + * with real connections — no mocked transports, no mocked inspections. + * + * Minimal mocks: only logger, auth/SSRF, cluster, mcpConfig, and DB repo + * (to avoid MongoDB). Everything else — the inspector, registry, cache, + * initializer, and MCP connection — runs for real. + */ + +import * as net from 'net'; +import * as http from 'http'; +import { Agent } from 'undici'; +import { randomUUID } from 'crypto'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { Keyv } from 'keyv'; +import { Types } from 'mongoose'; +import type { IUser } from '@librechat/data-schemas'; +import type { Socket } from 'net'; +import type * as t from '~/mcp/types'; +import { MCPInspectionFailedError } from '~/mcp/errors'; +import { registryStatusCache } from '~/mcp/registry/cache/RegistryStatusCache'; +import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; +import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; +import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; +import { FlowStateManager } from '~/flow/manager'; +import { MCPConnection } from '~/mcp/connection'; +import { MCPManager } from '~/mcp/MCPManager'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/cluster', () => ({ + isLeader: jest.fn().mockResolvedValue(true), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0 }, +})); + +jest.mock('~/mcp/registry/db/ServerConfigsDB', () => ({ + ServerConfigsDB: jest.fn().mockImplementation(() => ({ + get: jest.fn().mockResolvedValue(undefined), + getAll: jest.fn().mockResolvedValue({}), + add: jest.fn().mockResolvedValue(undefined), + update: jest.fn().mockResolvedValue(undefined), + remove: jest.fn().mockResolvedValue(undefined), + reset: jest.fn().mockResolvedValue(undefined), + })), +})); + +const mockMongoose = {} as typeof import('mongoose'); + +const allAgentsCreated: Agent[] = []; +const OriginalAgent = Agent; +const PatchedAgent = new Proxy(OriginalAgent, { + construct(target, args) { + const instance = new target(...(args as [Agent.Options?])); + allAgentsCreated.push(instance); + return instance; + }, +}); +(global as Record).__undiciAgent = PatchedAgent; + +afterAll(async () => { + const destroying = allAgentsCreated.map((a) => { + if (!a.destroyed && !a.closed) { + return a.destroy().catch(() => undefined); + } + return Promise.resolve(); + }); + allAgentsCreated.length = 0; + await Promise.all(destroying); +}); + +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) return; + (conn as unknown as { shouldStopReconnecting: boolean }).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) socket.destroy(); + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +interface TestServer { + url: string; + port: number; + close: () => Promise; +} + +async function createMCPServerOnPort(port: number): Promise { + const sessions = new Map(); + + const httpServer = http.createServer(async (req, res) => { + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); + const mcp = new McpServer({ name: 'recovery-test-server', version: '0.0.1' }); + mcp.tool('echo', 'Echo tool for testing', {}, async () => ({ + content: [{ type: 'text', text: 'ok' }], + })); + mcp.tool('greet', 'Greeting tool', {}, async () => ({ + content: [{ type: 'text', text: 'hello' }], + })); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + port, + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +describe('MCP reinitialize recovery – integration (issue #12143)', () => { + let server: TestServer | null = null; + let conn: MCPConnection | null = null; + let registry: MCPServersRegistry; + + beforeEach(async () => { + (MCPServersRegistry as unknown as { instance: undefined }).instance = undefined; + MCPServersRegistry.createInstance(mockMongoose, ['127.0.0.1']); + registry = MCPServersRegistry.getInstance(); + await registryStatusCache.reset(); + await registry.reset(); + MCPServersInitializer.resetProcessFlag(); + }); + + afterEach(async () => { + await safeDisconnect(conn); + conn = null; + // Reset MCPManager if it was created during the test + try { + const mgr = MCPManager.getInstance(); + await Promise.all(mgr.appConnections?.disconnectAll() ?? []); + } catch { + // Not initialized — nothing to clean up + } + (MCPManager as unknown as { instance: null }).instance = null; + if (server) { + await server.close(); + server = null; + } + }); + + it('should store a stub config when the MCP server is unreachable at startup', async () => { + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'speedy-mcp': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + + // Before the fix: getServerConfig would return undefined here + // After the fix: a stub with inspectionFailed=true is stored + const config = await registry.getServerConfig('speedy-mcp'); + expect(config).toBeDefined(); + expect(config!.inspectionFailed).toBe(true); + expect(config!.url).toBe(`http://127.0.0.1:${deadPort}/`); + expect(config!.tools).toBeUndefined(); + expect(config!.capabilities).toBeUndefined(); + expect(config!.toolFunctions).toBeUndefined(); + }); + + it('should recover via reinspectServer after the MCP server comes back online', async () => { + // Phase 1: Server is down at startup + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'speedy-mcp': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + + const stubConfig = await registry.getServerConfig('speedy-mcp'); + expect(stubConfig).toBeDefined(); + expect(stubConfig!.inspectionFailed).toBe(true); + + // Phase 2: Start the real server on the same (previously dead) port + server = await createMCPServerOnPort(deadPort); + + // Phase 3: Reinspect — this is what the reinitialize button triggers + const result = await registry.reinspectServer('speedy-mcp', 'CACHE'); + + // Verify the stub was replaced with a fully inspected config + expect(result.config.inspectionFailed).toBeUndefined(); + expect(result.config.tools).toContain('echo'); + expect(result.config.tools).toContain('greet'); + expect(result.config.capabilities).toBeDefined(); + expect(result.config.toolFunctions).toBeDefined(); + + // Verify the registry now returns the real config + const realConfig = await registry.getServerConfig('speedy-mcp'); + expect(realConfig).toBeDefined(); + expect(realConfig!.inspectionFailed).toBeUndefined(); + expect(realConfig!.tools).toContain('echo'); + }); + + it('should allow a real client connection after reinspection succeeds', async () => { + // Phase 1: Server is down at startup + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'speedy-mcp': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + expect((await registry.getServerConfig('speedy-mcp'))!.inspectionFailed).toBe(true); + + // Phase 2: Server comes back online on the same port + server = await createMCPServerOnPort(deadPort); + + // Phase 3: Reinspect + await registry.reinspectServer('speedy-mcp', 'CACHE'); + + // Phase 4: Establish a real client connection + conn = new MCPConnection({ + serverName: 'speedy-mcp', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + const tools = await conn.fetchTools(); + + expect(tools).toHaveLength(2); + expect(tools.map((t) => t.name)).toContain('echo'); + expect(tools.map((t) => t.name)).toContain('greet'); + }); + + it('should not attempt connections to stub servers via ConnectionsRepository', async () => { + const deadPort = await getFreePort(); + await MCPServersInitializer.initialize({ + 'stub-srv': { type: 'streamable-http', url: `http://127.0.0.1:${deadPort}/` }, + }); + expect((await registry.getServerConfig('stub-srv'))!.inspectionFailed).toBe(true); + + const repo = new ConnectionsRepository(undefined); + expect(await repo.has('stub-srv')).toBe(false); + expect(await repo.get('stub-srv')).toBeNull(); + + const all = await repo.getAll(); + expect(all.has('stub-srv')).toBe(false); + }); + + it('addServerStub should clear negative read-through cache entries', async () => { + // Query a server that doesn't exist — result is negative-cached + const config1 = await registry.getServerConfig('late-server'); + expect(config1).toBeUndefined(); + + // Store a stub (simulating a failed init that runs after the lookup) + await registry.addServerStub( + 'late-server', + { type: 'streamable-http', url: 'http://127.0.0.1:9999/' }, + 'CACHE', + ); + + // The stub should be found despite the earlier negative cache entry + const config2 = await registry.getServerConfig('late-server'); + expect(config2).toBeDefined(); + expect(config2!.inspectionFailed).toBe(true); + }); + + it('concurrent reinspectServer calls should not crash or corrupt state', async () => { + const deadPort = await getFreePort(); + await MCPServersInitializer.initialize({ + 'race-server': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }); + expect((await registry.getServerConfig('race-server'))!.inspectionFailed).toBe(true); + + server = await createMCPServerOnPort(deadPort); + + // Simulate multiple users clicking Reinitialize at the same time. + // reinitMCPServer calls reinspectServer internally — this tests the critical section. + const n = 3 + Math.floor(Math.random() * 8); // 3–10 concurrent calls + const results = await Promise.allSettled( + Array.from({ length: n }, () => registry.reinspectServer('race-server', 'CACHE')), + ); + + const successes = results.filter((r) => r.status === 'fulfilled'); + const failures = results.filter((r) => r.status === 'rejected'); + + // At least one must succeed + expect(successes.length).toBeGreaterThanOrEqual(1); + + // Any failure must be the "not in a failed state" guard (the first call already + // replaced the stub), not an unhandled crash or data corruption. + for (const f of failures) { + expect((f as PromiseRejectedResult).reason.message).toMatch(/not in a failed state/); + } + + // Final state must be fully recovered regardless of how many succeeded + const config = await registry.getServerConfig('race-server'); + expect(config).toBeDefined(); + expect(config!.inspectionFailed).toBeUndefined(); + expect(config!.tools).toContain('echo'); + }); + + it('concurrent reinitMCPServer-equivalent flows should not crash or corrupt state', async () => { + const deadPort = await getFreePort(); + const serverName = 'concurrent-reinit'; + const configs: t.MCPServers = { + [serverName]: { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + // Reset MCPManager singleton so createInstance works + (MCPManager as unknown as { instance: null }).instance = null; + + // Initialize with dead server — this sets up both registry (stub) and MCPManager + await MCPManager.createInstance(configs); + const mcpManager = MCPManager.getInstance(); + + expect((await registry.getServerConfig(serverName))!.inspectionFailed).toBe(true); + + // Server comes back online + server = await createMCPServerOnPort(deadPort); + + const flowManager = new FlowStateManager(new Keyv(), { ttl: 60_000 }); + const makeUser = (): IUser => + ({ + _id: new Types.ObjectId(), + id: new Types.ObjectId().toString(), + username: 'testuser', + email: 'test@example.com', + name: 'Test', + avatar: '', + provider: 'email', + role: 'user', + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }) as IUser; + + /** + * Replicate reinitMCPServer logic: check inspectionFailed → reinspect → getConnection. + * Each call uses a distinct user to simulate concurrent requests from different users. + */ + async function simulateReinitMCPServer(): Promise<{ success: boolean; tools: number }> { + const user = makeUser(); + const config = await registry.getServerConfig(serverName, user.id); + if (config?.inspectionFailed) { + try { + const storageLocation = config.dbId ? 'DB' : 'CACHE'; + await registry.reinspectServer(serverName, storageLocation, user.id); + } catch { + // Mirrors reinitMCPServer early return on failed reinspection + return { success: false, tools: 0 }; + } + } + + const connection = await mcpManager.getConnection({ + serverName, + user, + flowManager, + forceNew: true, + }); + + const tools = await connection.fetchTools(); + return { success: true, tools: tools.length }; + } + + const n = 3 + Math.floor(Math.random() * 5); // 3–7 concurrent calls + const results = await Promise.allSettled( + Array.from({ length: n }, () => simulateReinitMCPServer()), + ); + + // All promises should resolve (no unhandled throws) + for (const r of results) { + expect(r.status).toBe('fulfilled'); + } + + const values = (results as PromiseFulfilledResult<{ success: boolean; tools: number }>[]).map( + (r) => r.value, + ); + + // At least one full reinit must succeed with tools + const succeeded = values.filter((v) => v.success); + expect(succeeded.length).toBeGreaterThanOrEqual(1); + for (const s of succeeded) { + expect(s.tools).toBe(2); + } + + // Any that returned success: false hit the reinspect guard — that's fine + const earlyReturned = values.filter((v) => !v.success); + expect(earlyReturned.every((v) => v.tools === 0)).toBe(true); + + // Final registry state must be fully recovered + const finalConfig = await registry.getServerConfig(serverName); + expect(finalConfig).toBeDefined(); + expect(finalConfig!.inspectionFailed).toBeUndefined(); + expect(finalConfig!.tools).toContain('echo'); + }); + + it('reinspectServer should throw MCPInspectionFailedError when the server is still unreachable', async () => { + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'still-broken': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + expect((await registry.getServerConfig('still-broken'))!.inspectionFailed).toBe(true); + + // Server is STILL down — reinspection should fail with MCPInspectionFailedError + await expect(registry.reinspectServer('still-broken', 'CACHE')).rejects.toThrow( + MCPInspectionFailedError, + ); + + // The stub should remain intact for future retry + const config = await registry.getServerConfig('still-broken'); + expect(config).toBeDefined(); + expect(config!.inspectionFailed).toBe(true); + }); +}); diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts index cb43cb68ce..12d2c9091f 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts @@ -303,9 +303,10 @@ describe('MCPServersInitializer Redis Integration Tests', () => { const searchToolsServer = await registry.getServerConfig('search_tools_server'); expect(searchToolsServer).toBeDefined(); - // Verify file_tools_server was not added (due to inspection failure) + // Verify file_tools_server was stored as a stub (for recovery via reinitialize) const fileToolsServer = await registry.getServerConfig('file_tools_server'); - expect(fileToolsServer).toBeUndefined(); + expect(fileToolsServer).toBeDefined(); + expect(fileToolsServer?.inspectionFailed).toBe(true); }); it('should set initialized status after completion', async () => { diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts index 255ef20760..2998b47d0b 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts @@ -1,11 +1,11 @@ import { logger } from '@librechat/data-schemas'; import * as t from '~/mcp/types'; -import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; -import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; -import { MCPConnection } from '~/mcp/connection'; import { registryStatusCache } from '~/mcp/registry/cache/RegistryStatusCache'; +import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector'; import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; +import { MCPConnection } from '~/mcp/connection'; const FIXED_TIME = 1699564800000; const originalDateNow = Date.now; @@ -296,9 +296,10 @@ describe('MCPServersInitializer', () => { const searchToolsServer = await registry.getServerConfig('search_tools_server'); expect(searchToolsServer).toBeDefined(); - // Verify file_tools_server was not added (due to inspection failure) + // Verify file_tools_server was stored as a stub (for recovery via reinitialize) const fileToolsServer = await registry.getServerConfig('file_tools_server'); - expect(fileToolsServer).toBeUndefined(); + expect(fileToolsServer).toBeDefined(); + expect(fileToolsServer?.inspectionFailed).toBe(true); }); it('should log server configuration after initialization', async () => { diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts index cc86f0e140..8891120717 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts @@ -1,4 +1,4 @@ -import * as t from '~/mcp/types'; +import type * as t from '~/mcp/types'; import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector'; @@ -193,6 +193,22 @@ describe('MCPServersRegistry', () => { }); }); + describe('reinspectServer', () => { + it('should throw when called on a healthy (non-stub) server', async () => { + await registry.addServer('healthy_server', testParsedConfig, 'CACHE'); + + await expect(registry.reinspectServer('healthy_server', 'CACHE')).rejects.toThrow( + 'is not in a failed state', + ); + }); + + it('should throw when the server does not exist', async () => { + await expect(registry.reinspectServer('ghost_server', 'CACHE')).rejects.toThrow( + 'not found in CACHE', + ); + }); + }); + describe('Read-through cache', () => { describe('getServerConfig', () => { it('should cache repeated calls for the same server', async () => { diff --git a/packages/api/src/mcp/types/index.ts b/packages/api/src/mcp/types/index.ts index 353bcb9c19..bbdabb4428 100644 --- a/packages/api/src/mcp/types/index.ts +++ b/packages/api/src/mcp/types/index.ts @@ -156,6 +156,8 @@ export type ParsedServerConfig = MCPOptions & { dbId?: string; /** True if access is only via agent (not directly shared with user) */ consumeOnly?: boolean; + /** True when inspection failed at startup; the server is known but not fully initialized */ + inspectionFailed?: boolean; }; export type AddServerResult = { From 873f446f8e6cf8946c77ee7853f0165b1a0dcf16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Airam=20Hern=C3=A1ndez=20Hern=C3=A1ndez?= <100208966+Airamhh@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:13:53 +0000 Subject: [PATCH 011/111] =?UTF-8?q?=F0=9F=95=B5=EF=B8=8F=20fix:=20`remoteA?= =?UTF-8?q?gents`=20Field=20Omitted=20from=20Config=20(#12150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: include remoteAgents config in loadDefaultInterface The loadDefaultInterface function was not passing the remoteAgents configuration from librechat.yaml to the permission system, causing remoteAgents permissions to never update from the YAML config even when explicitly configured. This fix adds the missing remoteAgents field to the returned loadedInterface object, allowing the permission update system to properly detect and apply remoteAgents configuration from the YAML file. Fixes remote agents (API) configuration not being applied from librechat.yaml * test: Add remoteAgents permission tests for USER and ADMIN roles Introduced new tests to validate the application of remoteAgents configuration in user permissions. The tests cover scenarios for explicit configuration, full enablement, and default role behavior when remoteAgents are not configured. This ensures that permissions are correctly applied based on the provided configuration, addressing a regression related to the omission of remoteAgents in the loadDefaultInterface function. --------- Co-authored-by: Airam Hernández Hernández Co-authored-by: Danny Avila --- packages/api/src/app/permissions.spec.ts | 94 ++++++++++++++++++++++ packages/data-schemas/src/app/interface.ts | 1 + 2 files changed, 95 insertions(+) diff --git a/packages/api/src/app/permissions.spec.ts b/packages/api/src/app/permissions.spec.ts index 41014dc200..7ab7e0d0d1 100644 --- a/packages/api/src/app/permissions.spec.ts +++ b/packages/api/src/app/permissions.spec.ts @@ -2100,4 +2100,98 @@ describe('updateInterfacePermissions - permissions', () => { expect(userCall[1][PermissionTypes.MCP_SERVERS]).toHaveProperty(Permissions.SHARE_PUBLIC); expect(userCall[1][PermissionTypes.MCP_SERVERS]).not.toHaveProperty(Permissions.SHARE); }); + + it('should apply explicit remoteAgents config to USER permissions (regression: loadDefaultInterface omission)', async () => { + const config = { + interface: { + remoteAgents: { use: true, create: true, share: false, public: false }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + expect(userCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + + expect(adminCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + }); + + it('should enable all remoteAgents permissions when fully enabled in config', async () => { + const config = { + interface: { + remoteAgents: { use: true, create: true, share: true, public: true }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }); + }); + + it('should use role defaults for remoteAgents when not configured (all false for USER)', async () => { + const config = { + interface: { + bookmarks: true, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + }); }); diff --git a/packages/data-schemas/src/app/interface.ts b/packages/data-schemas/src/app/interface.ts index f8afdefd33..1701a22fad 100644 --- a/packages/data-schemas/src/app/interface.ts +++ b/packages/data-schemas/src/app/interface.ts @@ -55,6 +55,7 @@ export async function loadDefaultInterface({ fileCitations: interfaceConfig?.fileCitations, peoplePicker: interfaceConfig?.peoplePicker, marketplace: interfaceConfig?.marketplace, + remoteAgents: interfaceConfig?.remoteAgents, }); return loadedInterface; From 9cf389715aa9752fa2240a87f15ef73bf096b297 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 9 Mar 2026 14:47:59 -0400 Subject: [PATCH 012/111] =?UTF-8?q?=F0=9F=93=A6=20chore:=20bump=20`mermaid?= =?UTF-8?q?`=20and=20`dompurify`=20(#12159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦 chore: bump `mermaid` and `dompurify` - Bump mermaid to version 11.13.0 in both package-lock.json and client/package.json. - Update monaco-editor to version 0.55.1 in both package-lock.json and client/package.json. - Upgrade @chevrotain packages to version 11.1.2 in package-lock.json. - Add dompurify as a dependency for monaco-editor in package.json. - Update d3-format to version 3.1.2 and dagre-d3-es to version 7.0.14 in package-lock.json. - Upgrade dompurify to version 3.3.2 in package-lock.json. * chore: update language prop in ArtifactCodeEditor for read-only mode for better UX - Adjusted the language prop in the MonacoEditor component to use 'plaintext' when in read-only mode, ensuring proper display of content without syntax highlighting. --- client/package.json | 4 +- .../Artifacts/ArtifactCodeEditor.tsx | 2 +- package-lock.json | 125 +++++++++--------- package.json | 3 + 4 files changed, 71 insertions(+), 63 deletions(-) diff --git a/client/package.json b/client/package.json index c588ccc6d9..760d539695 100644 --- a/client/package.json +++ b/client/package.json @@ -81,7 +81,7 @@ "lodash": "^4.17.23", "lucide-react": "^0.394.0", "match-sorter": "^8.1.0", - "mermaid": "^11.12.3", + "mermaid": "^11.13.0", "micromark-extension-llm-math": "^3.1.0", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", @@ -148,7 +148,7 @@ "jest-environment-jsdom": "^30.2.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.55.1", "postcss": "^8.4.31", "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", diff --git a/client/src/components/Artifacts/ArtifactCodeEditor.tsx b/client/src/components/Artifacts/ArtifactCodeEditor.tsx index ddf8dc84fa..d03397821d 100644 --- a/client/src/components/Artifacts/ArtifactCodeEditor.tsx +++ b/client/src/components/Artifacts/ArtifactCodeEditor.tsx @@ -313,7 +313,7 @@ export const ArtifactCodeEditor = function ArtifactCodeEditor({
=12" @@ -25322,9 +25332,9 @@ } }, "node_modules/dagre-d3-es": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", - "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -25848,10 +25858,13 @@ } }, "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -33495,27 +33508,28 @@ } }, "node_modules/mermaid": { - "version": "11.12.3", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", - "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.13.0.tgz", + "integrity": "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^1.0.0", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.0.1", "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.13", - "dayjs": "^1.11.18", - "dompurify": "^3.2.5", - "katex": "^0.16.22", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", - "marked": "^16.2.1", + "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", @@ -34312,15 +34326,6 @@ "marked": "14.0.0" } }, - "node_modules/monaco-editor/node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/monaco-editor/node_modules/marked": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", diff --git a/package.json b/package.json index e0d5925e8c..fd2d5191e1 100644 --- a/package.json +++ b/package.json @@ -172,6 +172,9 @@ "underscore": "1.13.8", "hono": "^4.12.4", "@hono/node-server": "^1.19.10", + "monaco-editor": { + "dompurify": "3.3.2" + }, "serialize-javascript": "^7.0.3", "svgo": "^2.8.2" }, From cfbe812d63451c1578faa9a13b7e77e0c9a9789b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 9 Mar 2026 15:19:57 -0400 Subject: [PATCH 013/111] =?UTF-8?q?=E2=9C=A8=20v0.8.3=20(#12161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ v0.8.3 * chore: Bump package versions and update configuration - Updated package versions for @librechat/api (1.7.25), @librechat/client (0.4.54), librechat-data-provider (0.8.302), and @librechat/data-schemas (0.0.38). - Incremented configuration version in librechat.example.yaml to 1.3.6. * feat: Add OpenRouter headers to OpenAI configuration - Introduced 'X-OpenRouter-Title' and 'X-OpenRouter-Categories' headers in the OpenAI configuration for enhanced compatibility with OpenRouter services. - Updated related tests to ensure the new headers are correctly included in the configuration responses. * chore: Update package versions and dependencies - Bumped versions for several dependencies including @eslint/eslintrc to 3.3.4, axios to 1.13.5, express to 5.2.1, and lodash to 4.17.23. - Updated @librechat/backend and @librechat/frontend versions to 0.8.3. - Added new dependencies: turbo and mammoth. - Adjusted various other dependencies to their latest versions for improved compatibility and performance. --- Dockerfile | 2 +- Dockerfile.multi | 2 +- api/package.json | 2 +- bun.lock | 4216 +++++++---------- client/jest.config.cjs | 2 +- client/package.json | 2 +- e2e/jestSetup.js | 2 +- helm/librechat/Chart.yaml | 4 +- librechat.example.yaml | 2 +- package-lock.json | 16 +- package.json | 2 +- packages/api/package.json | 2 +- .../openai/config.backward-compat.spec.ts | 2 + .../api/src/endpoints/openai/config.spec.ts | 4 + packages/api/src/endpoints/openai/config.ts | 2 + packages/client/package.json | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/config.ts | 4 +- packages/data-schemas/package.json | 2 +- 19 files changed, 1850 insertions(+), 2422 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5019060ab7..bbff8133da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# v0.8.3-rc2 +# v0.8.3 # Base node image FROM node:20-alpine AS node diff --git a/Dockerfile.multi b/Dockerfile.multi index 9178c184cb..53810b5f0a 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -1,5 +1,5 @@ # Dockerfile.multi -# v0.8.3-rc2 +# v0.8.3 # Set configurable max-old-space-size with default ARG NODE_MAX_OLD_SPACE_SIZE=6144 diff --git a/api/package.json b/api/package.json index 2f4bca3a0c..fcd353af57 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/backend", - "version": "v0.8.3-rc2", + "version": "v0.8.3", "description": "", "scripts": { "start": "echo 'please run this from the root directory'", diff --git a/bun.lock b/bun.lock index 622489ea0f..39d9641ec4 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "devDependencies": { "@axe-core/playwright": "^4.10.1", "@eslint/compat": "^1.2.6", - "@eslint/eslintrc": "^3.3.1", + "@eslint/eslintrc": "^3.3.4", "@eslint/js": "^9.20.0", "@playwright/test": "^1.56.1", "@types/react-virtualized": "^9.22.0", @@ -31,30 +31,32 @@ "lint-staged": "^15.4.3", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", + "turbo": "^2.8.12", "typescript-eslint": "^8.24.0", }, }, "api": { "name": "@librechat/backend", - "version": "0.8.300", + "version": "0.8.3", "dependencies": { - "@aws-sdk/client-bedrock-runtime": "^3.941.0", - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.980.0", + "@aws-sdk/client-s3": "^3.980.0", "@aws-sdk/s3-request-presigner": "^3.758.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", - "@googleapis/youtube": "^20.0.0", + "@azure/storage-blob": "^12.30.0", + "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@langchain/core": "^0.3.79", - "@librechat/agents": "^3.0.50", + "@langchain/core": "^0.3.80", + "@librechat/agents": "^3.1.55", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", - "@modelcontextprotocol/sdk": "^1.24.3", + "@modelcontextprotocol/sdk": "^1.27.1", "@node-saml/passport-saml": "^5.1.0", "@smithy/node-http-handler": "^4.4.5", - "axios": "^1.12.1", + "axios": "^1.13.5", "bcryptjs": "^2.4.3", "compression": "^1.8.1", "connect-redis": "^8.1.0", @@ -64,9 +66,9 @@ "dedent": "^1.5.3", "dotenv": "^16.0.3", "eventsource": "^3.0.2", - "express": "^5.1.0", + "express": "^5.2.1", "express-mongo-sanitize": "^2.2.0", - "express-rate-limit": "^8.2.1", + "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", "file-type": "^18.7.0", @@ -82,13 +84,15 @@ "keyv-file": "^5.1.2", "klona": "^2.0.6", "librechat-data-provider": "*", - "lodash": "^4.17.21", + "lodash": "^4.17.23", + "mammoth": "^1.11.0", + "mathjs": "^15.1.0", "meilisearch": "^0.38.0", "memorystore": "^1.6.7", "mime": "^3.0.0", "module-alias": "^2.2.3", "mongoose": "^8.12.1", - "multer": "^2.0.2", + "multer": "^2.1.1", "nanoid": "^3.3.7", "node-fetch": "^2.7.0", "nodemailer": "^7.0.11", @@ -104,15 +108,16 @@ "passport-jwt": "^4.0.1", "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "sharp": "^0.33.5", "tiktoken": "^1.0.15", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "undici": "^7.10.0", + "undici": "^7.18.2", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", - "youtube-transcript": "^1.2.1", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "^3.22.4", }, "devDependencies": { @@ -124,7 +129,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "0.8.300", + "version": "0.8.3", "dependencies": { "@ariakit/react": "^0.4.15", "@ariakit/react-core": "^0.4.17", @@ -135,11 +140,12 @@ "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@mcp-ui/client": "^5.7.0", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dialog": "1.0.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", @@ -174,9 +180,10 @@ "jotai": "^2.12.5", "js-cookie": "^3.0.5", "librechat-data-provider": "*", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "lucide-react": "^0.394.0", "match-sorter": "^8.1.0", + "mermaid": "^11.13.0", "micromark-extension-llm-math": "^3.1.0", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", @@ -189,10 +196,9 @@ "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", "react-i18next": "^15.4.0", - "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^3.0.6", - "react-router-dom": "^6.11.2", + "react-router-dom": "^6.30.3", "react-speech-recognition": "^3.10.0", "react-textarea-autosize": "^8.4.0", "react-transition-group": "^4.4.5", @@ -206,9 +212,11 @@ "remark-math": "^6.0.0", "remark-supersub": "^1.0.0", "sse.js": "^2.5.0", + "swr": "^2.3.8", "tailwind-merge": "^1.9.1", "tailwindcss-animate": "^1.0.5", "tailwindcss-radix": "^2.8.0", + "ts-md5": "^1.3.1", "zod": "^3.22.4", }, "devDependencies": { @@ -216,6 +224,7 @@ "@babel/preset-env": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.22.15", + "@happy-dom/jest-environment": "^20.8.3", "@tanstack/react-query-devtools": "^4.29.0", "@testing-library/dom": "^9.3.0", "@testing-library/jest-dom": "^5.16.5", @@ -224,10 +233,10 @@ "@types/jest": "^29.5.14", "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.17.15", - "@types/node": "^20.3.0", + "@types/node": "^20.19.35", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.1.4", "autoprefixer": "^10.4.20", "babel-plugin-replace-ts-export-assignment": "^0.0.2", "babel-plugin-root-import": "^6.6.0", @@ -238,23 +247,23 @@ "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.2.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", + "monaco-editor": "^0.55.1", "postcss": "^8.4.31", - "postcss-loader": "^7.1.0", - "postcss-preset-env": "^8.2.0", + "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3", - "vite": "^6.4.1", + "vite": "^7.3.1", "vite-plugin-compression2": "^2.2.1", - "vite-plugin-node-polyfills": "^0.23.0", - "vite-plugin-pwa": "^0.21.2", + "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-pwa": "^1.2.0", }, }, "packages/api": { "name": "@librechat/api", - "version": "1.7.24", + "version": "1.7.25", "devDependencies": { "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", @@ -266,7 +275,6 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-typescript": "^12.1.2", "@types/bun": "^1.2.15", - "@types/diff": "^6.0.0", "@types/express": "^5.0.0", "@types/express-session": "^1.18.2", "@types/jest": "^29.5.2", @@ -279,49 +287,60 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "librechat-data-provider": "*", + "mammoth": "^1.11.0", "mongodb": "^6.14.2", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "pdfjs-dist": "^5.4.624", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "ts-node": "^10.9.2", "typescript": "^5.0.4", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.970.0", + "@aws-sdk/client-s3": "^3.980.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", + "@azure/storage-blob": "^12.30.0", + "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@langchain/core": "^0.3.79", - "@librechat/agents": "^3.0.50", + "@langchain/core": "^0.3.80", + "@librechat/agents": "^3.1.55", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.24.3", - "axios": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.27.1", + "@smithy/node-http-handler": "^4.4.5", + "axios": "^1.13.5", "connect-redis": "^8.1.0", - "diff": "^7.0.0", "eventsource": "^3.0.2", "express": "^5.1.0", "express-session": "^1.18.2", "firebase": "^11.0.2", "form-data": "^4.0.4", + "google-auth-library": "^9.15.1", + "https-proxy-agent": "^7.0.6", "ioredis": "^5.3.2", "js-yaml": "^4.1.1", "jsonwebtoken": "^9.0.0", "keyv": "^5.3.2", "keyv-file": "^5.1.2", "librechat-data-provider": "*", + "mammoth": "^1.11.0", + "mathjs": "^15.1.0", "memorystore": "^1.6.7", "mongoose": "^8.12.1", "node-fetch": "2.7.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "tiktoken": "^1.0.15", - "undici": "^7.10.0", + "undici": "^7.18.2", "zod": "^3.22.4", }, }, "packages/client": { "name": "@librechat/client", - "version": "0.4.53", + "version": "0.4.54", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -351,8 +370,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^15.4.0", - "rimraf": "^6.1.2", - "rollup": "^4.0.0", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.35.0", @@ -366,10 +385,10 @@ "@dicebear/core": "^9.2.2", "@headlessui/react": "^2.1.2", "@radix-ui/react-accordion": "^1.2.11", - "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dialog": "1.0.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", @@ -409,9 +428,9 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.301", + "version": "0.8.302", "dependencies": { - "axios": "^1.12.1", + "axios": "^1.13.5", "dayjs": "^1.11.13", "js-yaml": "^4.1.1", "zod": "^3.22.4", @@ -420,7 +439,6 @@ "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", - "@langchain/core": "^0.3.62", "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", @@ -435,8 +453,8 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "openapi-types": "^12.1.3", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-typescript2": "^0.35.0", "typescript": "^5.0.4", @@ -447,7 +465,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.37", + "version": "0.0.38", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^29.0.0", @@ -456,15 +474,14 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.2", - "@types/diff": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.0", "jest": "^30.2.0", "jest-junit": "^16.0.0", "mongodb-memory-server": "^10.1.4", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-typescript2": "^0.35.0", "ts-node": "^10.9.2", @@ -474,7 +491,7 @@ "jsonwebtoken": "^9.0.2", "klona": "^2.0.6", "librechat-data-provider": "*", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "meilisearch": "^0.38.0", "mongoose": "^8.12.1", "nanoid": "^3.3.7", @@ -484,11 +501,20 @@ }, }, "overrides": { + "@anthropic-ai/sdk": "0.73.0", + "@hono/node-server": "^1.19.10", "axios": "1.12.1", "elliptic": "^6.6.1", + "fast-xml-parser": "5.3.8", "form-data": "^4.0.4", + "hono": "^4.12.4", "katex": "^0.16.21", + "langsmith": "0.4.12", "mdast-util-gfm-autolink-literal": "2.0.0", + "serialize-javascript": "^7.0.3", + "svgo": "^2.8.2", + "tslib": "^2.8.1", + "underscore": "1.13.8", }, "packages": { "@aashutoshrathi/word-wrap": ["@aashutoshrathi/word-wrap@1.2.6", "", {}, "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA=="], @@ -497,7 +523,11 @@ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.65.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.73.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw=="], + + "@anthropic-ai/vertex-sdk": ["@anthropic-ai/vertex-sdk@0.14.4", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "google-auth-library": "^9.4.2" } }, "sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g=="], "@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="], @@ -525,19 +555,21 @@ "@aws-sdk/client-bedrock-agent-runtime": ["@aws-sdk/client-bedrock-agent-runtime@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/credential-provider-node": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/eventstream-serde-browser": "^4.2.4", "@smithy/eventstream-serde-config-resolver": "^4.3.4", "@smithy/eventstream-serde-node": "^4.2.4", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k2UeG/+Ka74jztHDzYNrpNLDSsMCst+ph3+e7uAX5Jmo40tVKa+sVu4DkV3BIXuktc6jqM1ewtfPNug79kN6JQ=="], - "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.952.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/credential-provider-node": "3.952.0", "@aws-sdk/eventstream-handler-node": "3.936.0", "@aws-sdk/middleware-eventstream": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.948.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/middleware-websocket": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/token-providers": "3.952.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xc1xqIz/OdFd23UQ6cvROD+3tfvDpp5dabMqUYXFiKlk5psMNM9xhzLwWK7DE1tr1ra/dui77w8JOiLA1dC7AA=="], + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1004.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.18", "@aws-sdk/eventstream-handler-node": "^3.972.10", "@aws-sdk/middleware-eventstream": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/middleware-websocket": "^3.972.12", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw=="], "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/client-sso-oidc": "3.623.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-kGYnTzXTMGdjko5+GZ1PvWvfXA7quiOp5iMo5gbh5b55pzIdc918MHN0pvaqplVGWYlaFJF4YzxUT5Nbxd7Xeg=="], "@aws-sdk/client-kendra": ["@aws-sdk/client-kendra@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/credential-provider-node": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-DWyNlC6BFhzoDkyKZ3xv0BC/xcXF3Tpq6j6Z42DXO9KEUjiGmC3se9l/GFEVtRLh/DR4p7cTJsxzA2QNuthRNg=="], - "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.758.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/credential-provider-node": "3.758.0", "@aws-sdk/middleware-bucket-endpoint": "3.734.0", "@aws-sdk/middleware-expect-continue": "3.734.0", "@aws-sdk/middleware-flexible-checksums": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-location-constraint": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-sdk-s3": "3.758.0", "@aws-sdk/middleware-ssec": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/signature-v4-multi-region": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@aws-sdk/xml-builder": "3.734.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/eventstream-serde-browser": "^4.0.1", "@smithy/eventstream-serde-config-resolver": "^4.0.1", "@smithy/eventstream-serde-node": "^4.0.1", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-blob-browser": "^4.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/hash-stream-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/md5-js": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "@smithy/util-waiter": "^4.0.2", "tslib": "^2.6.2" } }, "sha512-f8SlhU9/93OC/WEI6xVJf/x/GoQFj9a/xXK6QCtr5fvCjfSLgMVFmKTiIl/tgtDRzxUDc8YS6EGtbHjJ3Y/atg=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1004.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.18", "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", "@aws-sdk/middleware-expect-continue": "^3.972.7", "@aws-sdk/middleware-flexible-checksums": "^3.973.4", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-location-constraint": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-sdk-s3": "^3.972.18", "@aws-sdk/middleware-ssec": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/signature-v4-multi-region": "^3.996.6", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-blob-browser": "^4.2.12", "@smithy/hash-node": "^4.2.11", "@smithy/hash-stream-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/md5-js": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-m0zNfpsona9jQdX1cHtHArOiuvSGZPsgp/KRZS2YjJhKah96G2UN3UNGZQ6aVjXIQjCY6UanCJo0uW9Xf2U41w=="], "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-oEACriysQMnHIVcNp7TD6D1nzgiHfYK0tmMBMbUxgoFuCBkW9g9QYvspHN+S9KgoePfMEXHuPUe9mtG9AH9XeA=="], "@aws-sdk/client-sso-oidc": ["@aws-sdk/client-sso-oidc@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-lMFEXCa6ES/FGV7hpyrppT1PiAkqQb51AbG0zVU3TIgI2IO4XX02uzMUXImRSRqRpGymRCbJCaCs9LtKvS/37Q=="], - "@aws-sdk/core": ["@aws-sdk/core@3.758.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg=="], + "@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], + + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw=="], "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.623.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.623.0", "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-sXU2KtWpFzIzE4iffSIUbl4mgbeN1Rta6BnuKtS3rrVrryku9akAxY//pulbsIsYfXRzOwZzULsa+cxQN00lrw=="], @@ -547,9 +579,9 @@ "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.623.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-kvXA1SwGneqGzFwRZNpESitnmaENHGFFuuTvgGwtMe7mzXWuA/LkXdbiHmdyAzOo0iByKTCD8uetuwh3CXy4Pw=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-jL9zc+e+7sZeJrHzYKK9GOjl1Ktinh0ORU3cM2uRBi7fuH/0zV9pdMN8PQnGXz0i4tJaKcZ1lrE4V0V6LB9NQg=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.758.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.758.0", "@aws-sdk/credential-provider-http": "3.758.0", "@aws-sdk/credential-provider-ini": "3.758.0", "@aws-sdk/credential-provider-process": "3.758.0", "@aws-sdk/credential-provider-sso": "3.758.0", "@aws-sdk/credential-provider-web-identity": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-+DaMv63wiq7pJrhIQzZYMn4hSarKiizDoJRvyR7WGhnn0oQ/getX9Z0VNCV3i7lIFoLNTb7WMmQ9k7+z/uD5EQ=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.18", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-ini": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw=="], "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.620.1", "", { "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg=="], @@ -559,57 +591,57 @@ "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.623.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.623.0", "@aws-sdk/client-sso": "3.623.0", "@aws-sdk/credential-provider-cognito-identity": "3.623.0", "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-ini": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-abtlH1hkVWAkzuOX79Q47l0ztWOV2Q7l7J4JwQgzEQm7+zCk5iUAiwqKyDzr+ByCyo4I3IWFjy+e1gBdL7rXQQ=="], - "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/eventstream-codec": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-4zIbhdRmol2KosIHmU31ATvNP0tkJhDlRj9GuawVJoEnMvJA1pd2U3SRdiOImJU3j8pT46VeS4YMmYxfjGHByg=="], + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA=="], - "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-etC7G18aF7KdZguW27GE/wpbrNmYLVT755EsFc8kXpZj8D6AFKxc7OuveinJmiy0bYXAMspJUWsF6CrGpOw6CQ=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA=="], - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-XQSH8gzLkk8CDUDxyt4Rdm9owTpRIPdtg2yw9Y2Wl5iSI55YQSiC3x8nM3c4Y4WqReJprunFPK225ZUDoYCfZA=="], + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g=="], - "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-P38/v1l6HjuB2aFUewt7ueAW5IvKkFcv5dalPtbMGRhLeyivBOHwbCyuRKgVs7z7ClTpu9EaViEGki2jEQqEsQ=="], + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ=="], - "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.758.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/is-array-buffer": "^4.0.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-o8Rk71S08YTKLoSobucjnbj97OCGaXgpEDNKXpXaavUM5xLNoHCLSUPRCiEN86Ivqxg1n17Y2nSRhfbsveOXXA=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.973.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/crc64-nvme": "^3.972.4", "@aws-sdk/types": "^3.973.5", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7CH2jcGmkvkHc5Buz9IGbdjq1729AAlgYJiAvGq7qhCHqYleCsriWdSnmsqWTwdAfXHMT+pkxX3w6v5tJNcSug=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], - "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-EJEIXwCQhto/cBfHdm3ZOeLxd2NlJD+X2F+ZTOxzokuhBtY0IONfC/91hOo5tWQweerojwshSMHRCKzRv1tlwg=="], + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], - "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-6mJ2zyyHPYSV6bAcaFpsdoXZJeQlR1QgBnZZ6juY/+dcYiuyWCdyLUbGzSZSE7GTfx6i+9+QWFeoIMlWKgU63A=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw=="], - "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-d4yd1RrPW/sspEXizq2NSOUivnheac6LPeLSLnaeTbBG9g1KqIqvCzP1TfXEqv2CrWfHEsWtJpX7oyjySSPvDQ=="], + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@smithy/core": "^3.1.5", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.8", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A=="], - "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/util-format-url": "3.936.0", "@smithy/eventstream-codec": "^4.2.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-bPe3rqeugyj/MmjP0yBSZox2v1Wa8Dv39KN+RxVbQroLO8VUitBo6xyZ0oZebhZ5sASwSg58aDcMlX0uFLQnTA=="], + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-format-url": "^3.972.7", "@smithy/eventstream-codec": "^4.2.11", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.952.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.948.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-OtuirjxuOqZyDcI0q4WtoyWfkq3nSnbH41JwJQsXJefduWcww1FQe5TL1JfYCU7seUxHzK8rg2nFxUBuqUlZtg=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.758.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-format-url": "3.734.0", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dVyItwu/J1InfJBbCPpHRV9jrsBfI7L0RlDGyS3x/xqBwnm5qpvgNZQasQiyqIl+WJB4f5rZRZHgHuwftqINbA=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.758.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-0RPCo8fYJcrenJ6bRtiUbFOSgQ1CX/GpvwtLU2Fam1tS9h2klKK8d74caeV6A1mIUvBU7bhyQ0wMGlwMtn3EYw=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.6", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.18", "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-IpQVC9WOeXQlCEcFVNXWDIKy92CH1Az37u9K0H3DF/HT56AjhyDVKQQfHUy00nt7bHFe3u0K5+zlwErBeKy5ZA=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1004.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA=="], - "@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], + "@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], - "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.723.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w=="], + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.743.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "@smithy/util-endpoints": "^3.0.1", "tslib": "^2.6.2" } }, "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-TxZMVm8V4aR/QkW9/NhujvYpPZjUYqzLwSge5imKZbWFR806NP7RMwc5ilVuHF/bMOln/cVHkl42kATElWBvNw=="], "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.568.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.758.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.4", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Zrjxi5qwGEcUsJ0ru7fRtW74WcTS0rbLcehoFB+rN1GRi2hbLcFaYs4PwVA5diLeAJH0gszv3x4Hr/S87MfbKQ=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="], @@ -647,7 +679,9 @@ "@azure/search-documents": ["@azure/search-documents@12.0.0", "", { "dependencies": { "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.3.0", "@azure/core-http-compat": "^2.0.1", "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.3.0", "@azure/core-tracing": "^1.0.0", "@azure/logger": "^1.0.0", "events": "^3.0.0", "tslib": "^2.2.0" } }, "sha512-d9d53f2WWBpLHifk+LVn+AG52zuXvjgxJAdaH6kuT2qwrO1natcigtTgBM8qrI3iDYaDXsQhJSIMEgg9WKSoWA=="], - "@azure/storage-blob": ["@azure/storage-blob@12.27.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.4.0", "@azure/core-client": "^1.6.2", "@azure/core-http-compat": "^2.0.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.10.1", "@azure/core-tracing": "^1.1.2", "@azure/core-util": "^1.6.1", "@azure/core-xml": "^1.4.3", "@azure/logger": "^1.0.0", "events": "^3.0.0", "tslib": "^2.2.0" } }, "sha512-IQjj9RIzAKatmNca3D6bT0qJ+Pkox1WZGOg2esJF2YLHb45pQKOwGPIAV+w3rfgkj7zV3RMxpn/c6iftzSOZJQ=="], + "@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="], + + "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], @@ -657,45 +691,33 @@ "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], - - "@babel/helper-builder-binary-assignment-operator-visitor": ["@babel/helper-builder-binary-assignment-operator-visitor@7.22.15", "", { "dependencies": { "@babel/types": "^7.22.15" } }, "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw=="], + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.26.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.26.9", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.26.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong=="], - "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], - - "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.22.20", "", {}, "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA=="], - - "@babel/helper-function-name": ["@babel/helper-function-name@7.23.0", "", { "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" } }, "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw=="], + "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - "@babel/helper-hoist-variables": ["@babel/helper-hoist-variables@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw=="], - - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="], + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="], + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], - "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-wrap-function": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw=="], + "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="], + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - "@babel/helper-simple-access": ["@babel/helper-simple-access@7.24.7", "", { "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" } }, "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="], - - "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.22.6", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g=="], + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -703,21 +725,21 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.25.9", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g=="], + "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g=="], + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], - "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw=="], + "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug=="], + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g=="], + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg=="], + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], @@ -729,11 +751,7 @@ "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], - "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], - - "@babel/plugin-syntax-export-namespace-from": ["@babel/plugin-syntax-export-namespace-from@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q=="], - - "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg=="], + "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], @@ -763,131 +781,131 @@ "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg=="], + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.26.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-remap-async-to-generator": "^7.25.9", "@babel/traverse": "^7.26.8" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg=="], + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-remap-async-to-generator": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ=="], + "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.26.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ=="], + "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg=="], + "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q=="], + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.26.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ=="], + "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9", "@babel/traverse": "^7.25.9", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg=="], + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/template": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA=="], + "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ=="], + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA=="], + "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw=="], + "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg=="], + "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ=="], - "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.26.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ=="], + "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww=="], + "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.26.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg=="], + "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA=="], + "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw=="], + "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ=="], + "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q=="], + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA=="], + "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw=="], + "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.26.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.26.0", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA=="], + "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw=="], + "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA=="], + "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ=="], + "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.26.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw=="], + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q=="], + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-transform-parameters": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg=="], + "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A=="], + "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g=="], + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A=="], + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g=="], + "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="], + "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw=="], + "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA=="], + "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw=="], + "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.23.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.23.3", "@babel/types": "^7.23.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA=="], + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.22.5", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A=="], + "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.23.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ=="], + "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "regenerator-transform": "^0.15.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg=="], + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.26.0", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw=="], + "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg=="], + "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.23.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "babel-plugin-polyfill-corejs2": "^0.4.8", "babel-plugin-polyfill-corejs3": "^0.9.0", "babel-plugin-polyfill-regenerator": "^0.5.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ=="], - "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng=="], + "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A=="], + "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA=="], + "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.26.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q=="], + "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.26.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw=="], + "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.23.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-create-class-features-plugin": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], - "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q=="], + "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg=="], + "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA=="], + "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ=="], + "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - "@babel/preset-env": ["@babel/preset-env@7.26.9", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.26.0", "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.25.9", "@babel/plugin-transform-async-generator-functions": "^7.26.8", "@babel/plugin-transform-async-to-generator": "^7.25.9", "@babel/plugin-transform-block-scoped-functions": "^7.26.5", "@babel/plugin-transform-block-scoping": "^7.25.9", "@babel/plugin-transform-class-properties": "^7.25.9", "@babel/plugin-transform-class-static-block": "^7.26.0", "@babel/plugin-transform-classes": "^7.25.9", "@babel/plugin-transform-computed-properties": "^7.25.9", "@babel/plugin-transform-destructuring": "^7.25.9", "@babel/plugin-transform-dotall-regex": "^7.25.9", "@babel/plugin-transform-duplicate-keys": "^7.25.9", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-dynamic-import": "^7.25.9", "@babel/plugin-transform-exponentiation-operator": "^7.26.3", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-for-of": "^7.26.9", "@babel/plugin-transform-function-name": "^7.25.9", "@babel/plugin-transform-json-strings": "^7.25.9", "@babel/plugin-transform-literals": "^7.25.9", "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", "@babel/plugin-transform-member-expression-literals": "^7.25.9", "@babel/plugin-transform-modules-amd": "^7.25.9", "@babel/plugin-transform-modules-commonjs": "^7.26.3", "@babel/plugin-transform-modules-systemjs": "^7.25.9", "@babel/plugin-transform-modules-umd": "^7.25.9", "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-new-target": "^7.25.9", "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", "@babel/plugin-transform-numeric-separator": "^7.25.9", "@babel/plugin-transform-object-rest-spread": "^7.25.9", "@babel/plugin-transform-object-super": "^7.25.9", "@babel/plugin-transform-optional-catch-binding": "^7.25.9", "@babel/plugin-transform-optional-chaining": "^7.25.9", "@babel/plugin-transform-parameters": "^7.25.9", "@babel/plugin-transform-private-methods": "^7.25.9", "@babel/plugin-transform-private-property-in-object": "^7.25.9", "@babel/plugin-transform-property-literals": "^7.25.9", "@babel/plugin-transform-regenerator": "^7.25.9", "@babel/plugin-transform-regexp-modifiers": "^7.26.0", "@babel/plugin-transform-reserved-words": "^7.25.9", "@babel/plugin-transform-shorthand-properties": "^7.25.9", "@babel/plugin-transform-spread": "^7.25.9", "@babel/plugin-transform-sticky-regex": "^7.25.9", "@babel/plugin-transform-template-literals": "^7.26.8", "@babel/plugin-transform-typeof-symbol": "^7.26.7", "@babel/plugin-transform-unicode-escapes": "^7.25.9", "@babel/plugin-transform-unicode-property-regex": "^7.25.9", "@babel/plugin-transform-unicode-regex": "^7.25.9", "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ=="], + "@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], - "@babel/preset-react": ["@babel/preset-react@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-transform-react-display-name": "^7.23.3", "@babel/plugin-transform-react-jsx": "^7.22.15", "@babel/plugin-transform-react-jsx-development": "^7.22.5", "@babel/plugin-transform-react-pure-annotations": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w=="], + "@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - "@babel/preset-typescript": ["@babel/preset-typescript@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-syntax-jsx": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@babel/plugin-transform-typescript": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ=="], + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], "@babel/runtime": ["@babel/runtime@7.26.10", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="], @@ -899,8 +917,20 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.2", "", { "dependencies": { "@chevrotain/gast": "11.1.2", "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q=="], + + "@chevrotain/gast": ["@chevrotain/gast@11.1.2", "", { "dependencies": { "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.2", "", {}, "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw=="], + + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + + "@chevrotain/utils": ["@chevrotain/utils@11.1.2", "", {}, "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" }, "peerDependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0" } }, "sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA=="], "@codemirror/commands": ["@codemirror/commands@6.6.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg=="], @@ -929,67 +959,109 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - "@csstools/cascade-layer-name-parser": ["@csstools/cascade-layer-name-parser@1.0.7", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ=="], + "@csstools/cascade-layer-name-parser": ["@csstools/cascade-layer-name-parser@3.0.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ=="], - "@csstools/color-helpers": ["@csstools/color-helpers@2.1.0", "", {}, "sha512-OWkqBa7PDzZuJ3Ha7T5bxdSVfSCfTq6K1mbAhbO1MD+GSULGjrp45i5RudyJOedstSarN/3mdwu9upJE7gDXfw=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], - "@csstools/css-calc": ["@csstools/css-calc@1.1.6", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-YHPAuFg5iA4qZGzMzvrQwzkvJpesXXyIUyaONflQrjtHB+BcFFbgltJkIkb31dMGO4SE9iZFA4HYpdk7+hnYew=="], + "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="], - "@csstools/css-color-parser": ["@csstools/css-color-parser@1.5.1", "", { "dependencies": { "@csstools/color-helpers": "^4.0.0", "@csstools/css-calc": "^1.1.6" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-x+SajGB2paGrTjPOUorGi8iCztF008YMKXTn+XzGVDBEIVJ/W1121pPerpneJYGOe1m6zWLPLnzOPaznmQxKFw=="], + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="], - "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@2.5.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ=="], + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], - "@csstools/css-tokenizer": ["@csstools/css-tokenizer@2.2.3", "", {}, "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg=="], + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - "@csstools/media-query-list-parser": ["@csstools/media-query-list-parser@2.1.7", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ=="], + "@csstools/media-query-list-parser": ["@csstools/media-query-list-parser@5.0.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg=="], - "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@3.0.1", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.2", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-dD8W98dOYNOH/yX4V4HXOhfCOnvVAg8TtsL+qCGNoKXuq5z2C/d026wGWgySgC8cajXXo/wNezS31Glj5GcqrA=="], + "@csstools/postcss-alpha-function": ["@csstools/postcss-alpha-function@2.0.3", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-8GqzD3JnfpKJSVxPIC0KadyAfB5VRzPZdv7XQ4zvK1q0ku+uHVUAS2N/IDavQkW40gkuUci64O0ea6QB/zgCSw=="], - "@csstools/postcss-color-function": ["@csstools/postcss-color-function@2.2.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-b1ptNkr1UWP96EEHqKBWWaV5m/0hgYGctgA/RVZhONeP1L3T/8hwoqDm9bB23yVCfOgE9U93KI9j06+pEkJTvw=="], + "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@6.0.0", "", { "dependencies": { "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-WhsECqmrEZQGqaPlBA7JkmF/CJ2/+wetL4fkL9sOPccKd32PQ1qToFM6gqSI5rkpmYqubvbxjEJhyMTHYK0vZQ=="], - "@csstools/postcss-color-mix-function": ["@csstools/postcss-color-mix-function@1.0.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-QGXjGugTluqFZWzVf+S3wCiRiI0ukXlYqCi7OnpDotP/zaVTyl/aqZujLFzTOXy24BoWnu89frGMc79ohY5eog=="], + "@csstools/postcss-color-function": ["@csstools/postcss-color-function@5.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-CjBdFemUFcAh3087MEJhZcO+QT1b8S75agysa1rU9TEC1YecznzwV+jpMxUc0JRBEV4ET2PjLssqmndR9IygeA=="], - "@csstools/postcss-font-format-keywords": ["@csstools/postcss-font-format-keywords@2.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-iKYZlIs6JsNT7NKyRjyIyezTCHLh4L4BBB3F5Nx7Dc4Z/QmBgX+YJFuUSar8IM6KclGiAUFGomXFdYxAwJydlA=="], + "@csstools/postcss-color-function-display-p3-linear": ["@csstools/postcss-color-function-display-p3-linear@2.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TWUwSe1+2KdYGGWTx5LR4JQN07vKHAeSho+bGYRgow+9cs3dqgOqS1f/a1odiX30ESmZvwIudJ86wzeiDR6UGg=="], - "@csstools/postcss-gradients-interpolation-method": ["@csstools/postcss-gradients-interpolation-method@3.0.6", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rBOBTat/YMmB0G8VHwKqDEx+RZ4KCU9j42K8LwS0IpZnyThalZZF7BCSsZ6TFlZhcRZKlZy3LLFI2pLqjNVGGA=="], + "@csstools/postcss-color-mix-function": ["@csstools/postcss-color-mix-function@4.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-PFKQKswFqZrYKpajZsP4lhqjU/6+J5PTOWq1rKiFnniKsf4LgpGXrgHS/C6nn5Rc51LX0n4dWOWqY5ZN2i5IjA=="], - "@csstools/postcss-hwb-function": ["@csstools/postcss-hwb-function@2.2.2", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-W5Y5oaJ382HSlbdGfPf60d7dAK6Hqf10+Be1yZbd/TNNrQ/3dDdV1c07YwOXPQ3PZ6dvFMhxbIbn8EC3ki3nEg=="], + "@csstools/postcss-color-mix-variadic-function-arguments": ["@csstools/postcss-color-mix-variadic-function-arguments@2.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-zEchsghpDH/6SytyjKu9TIPm4hiiWcur102cENl54cyIwTZsa+2MBJl/vtyALZ+uQ17h27L4waD+0Ow96sgZow=="], - "@csstools/postcss-ic-unit": ["@csstools/postcss-ic-unit@2.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^2.3.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9W2ZbV7whWnr1Gt4qYgxMWzbevZMOvclUczT5vk4yR6vS53W/njiiUhtm/jh/BKYwQ1W3PECZjgAd2dH4ebJig=="], + "@csstools/postcss-content-alt-text": ["@csstools/postcss-content-alt-text@3.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-OHa+4aCcrJtHpPWB3zptScHwpS1TUbeLR4uO0ntIz0Su/zw9SoWkVu+tDMSySSAsNtNSI3kut4fTliFwIsrHxA=="], - "@csstools/postcss-is-pseudo-class": ["@csstools/postcss-is-pseudo-class@3.2.1", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AtANdV34kJl04Al62is3eQRk/BfOfyAvEmRJvbt+nx5REqImLC+2XhuE6skgkcPli1l8ONS67wS+l1sBzySc3Q=="], + "@csstools/postcss-contrast-color-function": ["@csstools/postcss-contrast-color-function@3.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fwOz/m+ytFPz4aIph2foQS9nEDOdOjYcN5bgwbGR2jGUV8mYaeD/EaTVMHTRb/zqB65y2qNwmcFcE6VQty69Pw=="], - "@csstools/postcss-logical-float-and-clear": ["@csstools/postcss-logical-float-and-clear@1.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-eO9z2sMLddvlfFEW5Fxbjyd03zaO7cJafDurK4rCqyRt9P7aaWwha0LcSzoROlcZrw1NBV2JAp2vMKfPMQO1xw=="], + "@csstools/postcss-exponential-functions": ["@csstools/postcss-exponential-functions@3.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-WHJ52Uk0AVUIICEYRY9xFHJZAuq0ZVg0f8xzqUN2zRFrZvGgRPpFwxK7h9FWvqKIOueOwN6hnJD23A8FwsUiVw=="], - "@csstools/postcss-logical-resize": ["@csstools/postcss-logical-resize@1.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-x1ge74eCSvpBkDDWppl+7FuD2dL68WP+wwP2qvdUcKY17vJksz+XoE1ZRV38uJgS6FNUwC0AxrPW5gy3MxsDHQ=="], + "@csstools/postcss-font-format-keywords": ["@csstools/postcss-font-format-keywords@5.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-M1EjCe/J3u8fFhOZgRci74cQhJ7R0UFBX6T+WqoEvjrr8hVfMiV+HTYrzxLY5OW8YllvXYr5Q5t5OvJbsUSeDg=="], - "@csstools/postcss-logical-viewport-units": ["@csstools/postcss-logical-viewport-units@1.0.3", "", { "dependencies": { "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-6zqcyRg9HSqIHIPMYdt6THWhRmE5/tyHKJQLysn2TeDf/ftq7Em9qwMTx98t2C/7UxIsYS8lOiHHxAVjWn2WUg=="], + "@csstools/postcss-font-width-property": ["@csstools/postcss-font-width-property@1.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AvmySApdijbjYQuXXh95tb7iVnqZBbJrv3oajO927ksE/mDmJBiszm+psW8orL2lRGR8j6ZU5Uv9/ou2Z5KRKA=="], - "@csstools/postcss-media-minmax": ["@csstools/postcss-media-minmax@1.1.2", "", { "dependencies": { "@csstools/css-calc": "^1.1.6", "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3", "@csstools/media-query-list-parser": "^2.1.7" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-7qTRTJxW96u2yiEaTep1+8nto1O/rEDacewKqH+Riq5E6EsHTOmGHxkB4Se5Ic5xgDC4I05lLZxzzxnlnSypxA=="], + "@csstools/postcss-gamut-mapping": ["@csstools/postcss-gamut-mapping@3.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IrXAW3KQ3Sxm29C3/4mYQ/iA0Q5OH9YFOPQ2w24iIlXpD06A9MHvmQapP2vAGtQI3tlp2Xw5LIdm9F8khARfOA=="], - "@csstools/postcss-media-queries-aspect-ratio-number-values": ["@csstools/postcss-media-queries-aspect-ratio-number-values@1.0.4", "", { "dependencies": { "@csstools/css-parser-algorithms": "^2.2.0", "@csstools/css-tokenizer": "^2.1.1", "@csstools/media-query-list-parser": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IwyTbyR8E2y3kh6Fhrs251KjKBJeUPV5GlnUKnpU70PRFEN2DolWbf2V4+o/B9+Oj77P/DullLTulWEQ8uFtAA=="], + "@csstools/postcss-gradients-interpolation-method": ["@csstools/postcss-gradients-interpolation-method@6.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-saQHvD1PD/zCdn+kxCWCcQOdXZBljr8L6BKlCLs0w8GXYfo3SHdWL1HZQ+I1hVCPlU+MJPJJbZJjG/jHRJSlAw=="], - "@csstools/postcss-nested-calc": ["@csstools/postcss-nested-calc@2.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jbwrP8rN4e7LNaRcpx3xpMUjhtt34I9OV+zgbcsYAAk6k1+3kODXJBf95/JMYWhu9g1oif7r06QVUgfWsKxCFw=="], + "@csstools/postcss-hwb-function": ["@csstools/postcss-hwb-function@5.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-ChR0+pKc/2cs900jakiv8dLrb69aez5P3T+g+wfJx1j6mreAe8orKTiMrVBk+DZvCRqpdOA2m8VoFms64A3Dew=="], - "@csstools/postcss-normalize-display-values": ["@csstools/postcss-normalize-display-values@2.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TQT5g3JQ5gPXC239YuRK8jFceXF9d25ZvBkyjzBGGoW5st5sPXFVQS8OjYb9IJ/K3CdfK4528y483cgS2DJR/w=="], + "@csstools/postcss-ic-unit": ["@csstools/postcss-ic-unit@5.0.0", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-/ws5d6c4uKqfM9zIL3ugcGI+3fvZEOOkJHNzAyTAGJIdZ+aSL9BVPNlHGV4QzmL0vqBSCOdU3+rhcMEj3+KzYw=="], - "@csstools/postcss-oklab-function": ["@csstools/postcss-oklab-function@2.2.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AgJ2rWMnLCDcbSMTHSqBYn66DNLBym6JpBpCaqmwZ9huGdljjDRuH3DzOYzkgQ7Pm2K92IYIq54IvFHloUOdvA=="], + "@csstools/postcss-initial": ["@csstools/postcss-initial@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-UVUrFmrTQyLomVepnjWlbBg7GoscLmXLwYFyjbcEnmpeGW7wde6lNpx5eM3eVwZI2M+7hCE3ykYnAsEPLcLa+Q=="], - "@csstools/postcss-progressive-custom-properties": ["@csstools/postcss-progressive-custom-properties@2.3.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Zd8ojyMlsL919TBExQ1I0CTpBDdyCpH/yOdqatZpuC3sd22K4SwC7+Yez3Q/vmXMWSAl+shjNeFZ7JMyxMjK+Q=="], + "@csstools/postcss-is-pseudo-class": ["@csstools/postcss-is-pseudo-class@6.0.0", "", { "dependencies": { "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1Hdy/ykg9RDo8vU8RiM2o+RaXO39WpFPaIkHxlAEJFofle/lc33tdQMKhBk3jR/Fe+uZNLOs3HlowFafyFptVw=="], - "@csstools/postcss-relative-color-syntax": ["@csstools/postcss-relative-color-syntax@1.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-juCoVInkgH2TZPfOhyx6tIal7jW37L/0Tt+Vcl1LoxqQA9sxcg3JWYZ98pl1BonDnki6s/M7nXzFQHWsWMeHgw=="], + "@csstools/postcss-light-dark-function": ["@csstools/postcss-light-dark-function@3.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-s++V5/hYazeRUCYIn2lsBVzUsxdeC46gtwpgW6lu5U/GlPOS5UTDT14kkEyPgXmFbCvaWLREqV7YTMJq1K3G6w=="], - "@csstools/postcss-scope-pseudo-class": ["@csstools/postcss-scope-pseudo-class@2.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-6Pvo4uexUCXt+Hz5iUtemQAcIuCYnL+ePs1khFR6/xPgC92aQLJ0zGHonWoewiBE+I++4gXK3pr+R1rlOFHe5w=="], + "@csstools/postcss-logical-float-and-clear": ["@csstools/postcss-logical-float-and-clear@4.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-NGzdIRVj/VxOa/TjVdkHeyiJoDihONV0+uB0csUdgWbFFr8xndtfqK8iIGP9IKJzco+w0hvBF2SSk2sDSTAnOQ=="], - "@csstools/postcss-stepped-value-functions": ["@csstools/postcss-stepped-value-functions@2.1.1", "", { "dependencies": { "@csstools/css-calc": "^1.1.1", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-YCvdF0GCZK35nhLgs7ippcxDlRVe5QsSht3+EghqTjnYnyl3BbWIN6fYQ1dKWYTJ+7Bgi41TgqQFfJDcp9Xy/w=="], + "@csstools/postcss-logical-overflow": ["@csstools/postcss-logical-overflow@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-5cRg93QXVskM0MNepHpPcL0WLSf5Hncky0DrFDQY/4ozbH5lH7SX5ejayVpNTGSX7IpOvu7ykQDLOdMMGYzwpA=="], - "@csstools/postcss-text-decoration-shorthand": ["@csstools/postcss-text-decoration-shorthand@2.2.4", "", { "dependencies": { "@csstools/color-helpers": "^2.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-zPN56sQkS/7YTCVZhOBVCWf7AiNge8fXDl7JVaHLz2RyT4pnyK2gFjckWRLpO0A2xkm1lCgZ0bepYZTwAVd/5A=="], + "@csstools/postcss-logical-overscroll-behavior": ["@csstools/postcss-logical-overscroll-behavior@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-82Jnl/5Wi5jb19nQE1XlBHrZcNL3PzOgcj268cDkfwf+xi10HBqufGo1Unwf5n8bbbEFhEKgyQW+vFsc9iY1jw=="], - "@csstools/postcss-trigonometric-functions": ["@csstools/postcss-trigonometric-functions@2.1.1", "", { "dependencies": { "@csstools/css-calc": "^1.1.1", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-XcXmHEFfHXhvYz40FtDlA4Fp4NQln2bWTsCwthd2c+MCnYArUYU3YaMqzR5CrKP3pMoGYTBnp5fMqf1HxItNyw=="], + "@csstools/postcss-logical-resize": ["@csstools/postcss-logical-resize@4.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-L0T3q0gei/tGetCGZU0c7VN77VTivRpz1YZRNxjXYmW+85PKeI6U9YnSvDqLU2vBT2uN4kLEzfgZ0ThIZpN18A=="], - "@csstools/postcss-unset-value": ["@csstools/postcss-unset-value@2.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-oJ9Xl29/yU8U7/pnMJRqAZd4YXNCfGEdcP4ywREuqm/xMqcgDNDppYRoCGDt40aaZQIEKBS79LytUDN/DHf0Ew=="], + "@csstools/postcss-logical-viewport-units": ["@csstools/postcss-logical-viewport-units@4.0.0", "", { "dependencies": { "@csstools/css-tokenizer": "^4.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TA3AqVN/1IH3dKRC2UUWvprvwyOs2IeD7FDZk5Hz20w4q33yIuSg0i0gjyTUkcn90g8A4n7QpyZ2AgBrnYPnnA=="], - "@csstools/selector-specificity": ["@csstools/selector-specificity@2.2.0", "", { "peerDependencies": { "postcss-selector-parser": "^6.0.10" } }, "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw=="], + "@csstools/postcss-media-minmax": ["@csstools/postcss-media-minmax@3.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-I+CrmZt23fyejMItpLQFOg9gPXkDBBDjTqRT0UxCTZlYZfGrzZn4z+2kbXLRwDfR59OK8zaf26M4kwYwG0e1MA=="], + + "@csstools/postcss-media-queries-aspect-ratio-number-values": ["@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FDdC3lbrj8Vr0SkGIcSLTcRB7ApG6nlJFxOxkEF2C5hIZC1jtgjISFSGn/WjFdVkn8Dqe+Vx9QXI3axS2w1XHw=="], + + "@csstools/postcss-mixins": ["@csstools/postcss-mixins@1.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rz6qjT2w9L3k65jGc2dX+3oGiSrYQ70EZPDrINSmSVoVys7lLBFH0tvEa8DW2sr9cbRVD/W+1sy8+7bfu0JUfg=="], + + "@csstools/postcss-nested-calc": ["@csstools/postcss-nested-calc@5.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-aPSw8P60e/i9BEfugauhikBqgjiwXcw3I9o4vXs+hktl4NSTgZRI0QHimxk9mst8N01A2TKDBxOln3mssRxiHQ=="], + + "@csstools/postcss-normalize-display-values": ["@csstools/postcss-normalize-display-values@5.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FcbEmoxDEGYvm2W3rQzVzcuo66+dDJjzzVDs+QwRmZLHYofGmMGwIKPqzF86/YW+euMDa7sh1xjWDvz/fzByZQ=="], + + "@csstools/postcss-oklab-function": ["@csstools/postcss-oklab-function@5.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-3d/Wcnp2uW6Io0Tajl0croeUo46gwOVQI9N32PjA/HVQo6z1iL7yp19Gp+6e5E5CDKGpW7U822MsDVo2XK1z0Q=="], + + "@csstools/postcss-position-area-property": ["@csstools/postcss-position-area-property@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-TeEfzsJGB23Syv7yCm8AHCD2XTFujdjr9YYu9ebH64vnfCEvY4BG319jXAYSlNlf3Yc9PNJ6WnkDkUF5XVgSKQ=="], + + "@csstools/postcss-progressive-custom-properties": ["@csstools/postcss-progressive-custom-properties@5.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-NsJoZ89rxmDrUsITf8QIk5w+lQZQ8Xw5K6cLFG+cfiffsLYHb3zcbOOrHLetGl1WIhjWWQ4Cr8MMrg46Q+oACg=="], + + "@csstools/postcss-property-rule-prelude-list": ["@csstools/postcss-property-rule-prelude-list@2.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-qcMAkc9AhpzHgmQCD8hoJgGYifcOAxd1exXjjxilMM6euwRE619xDa4UsKBCv/v4g+sS63sd6c29LPM8s2ylSQ=="], + + "@csstools/postcss-random-function": ["@csstools/postcss-random-function@3.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-SvKGfmj+WHfn4bWHaBYlkXDyU3SlA3fL8aaYZ8Op6M8tunNf3iV9uZyZZGWMCbDw0sGeoTmYZW9nmKN8Qi/ctg=="], + + "@csstools/postcss-relative-color-syntax": ["@csstools/postcss-relative-color-syntax@4.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-HaMN+qMURinllszbps2AhXKaLeibg/2VW6FriYDrqE58ji82+z2S3/eLloywVOY8BQCJ9lZMdy6TcRQNbn9u3w=="], + + "@csstools/postcss-scope-pseudo-class": ["@csstools/postcss-scope-pseudo-class@5.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-kBrBFJcAji3MSHS4qQIihPvJfJC5xCabXLbejqDMiQi+86HD4eMBiTayAo46Urg7tlEmZZQFymFiJt+GH6nvXw=="], + + "@csstools/postcss-sign-functions": ["@csstools/postcss-sign-functions@2.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-C3br0qcHJkQ0qSGUBnDJHXQdO8XObnCpGwai5m1L2tv2nCjt0vRHG6A9aVCQHvh08OqHNM2ty1dYDNNXV99YAQ=="], + + "@csstools/postcss-stepped-value-functions": ["@csstools/postcss-stepped-value-functions@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-vZf7zPzRb7xIi2o5Z9q6wyeEAjoRCg74O2QvYxmQgxYO5V5cdBv4phgJDyOAOP3JHy4abQlm2YaEUS3gtGQo0g=="], + + "@csstools/postcss-syntax-descriptor-syntax-production": ["@csstools/postcss-syntax-descriptor-syntax-production@2.0.0", "", { "dependencies": { "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-elYcbdiBXAkPqvojB9kIBRuHY6htUhjSITtFQ+XiXnt6SvZCbNGxQmaaw6uZ7SPHu/+i/XVjzIt09/1k3SIerQ=="], + + "@csstools/postcss-system-ui-font-family": ["@csstools/postcss-system-ui-font-family@2.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FyGZCgchFImFyiHS2x3rD5trAqatf/x23veBLTIgbaqyFfna6RNBD+Qf8HRSjt6HGMXOLhAjxJ3OoZg0bbn7Qw=="], + + "@csstools/postcss-text-decoration-shorthand": ["@csstools/postcss-text-decoration-shorthand@5.0.3", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-62fjggvIM1YYfDJPcErMUDkEZB6CByG8neTJqexnZe1hRBgCjD4dnXDLoCSSurjs1LzjBq6irFDpDaOvDZfrlw=="], + + "@csstools/postcss-trigonometric-functions": ["@csstools/postcss-trigonometric-functions@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-e8me32Mhl8JeBnxVJgsQUYpV4Md4KiyvpILpQlaY/eK1Gwdb04kasiTTswPQ5q7Z8+FppJZ2Z4d8HRfn6rjD3w=="], + + "@csstools/postcss-unset-value": ["@csstools/postcss-unset-value@5.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-EoO54sS2KCIfesvHyFYAW99RtzwHdgaJzhl7cqKZSaMYKZv3fXSOehDjAQx8WZBKn1JrMd7xJJI1T1BxPF7/jA=="], + + "@csstools/selector-resolve-nested": ["@csstools/selector-resolve-nested@4.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA=="], + + "@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA=="], + + "@csstools/utilities": ["@csstools/utilities@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-etDqA/4jYvOGBM6yfKCOsEXfH96BKztZdgGmGqKi2xHnDe0ILIBraRspwgYatJH9JsCZ5HCGoCst8w18EKOAdg=="], "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], @@ -1063,55 +1135,57 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.1", "", { "os": "none", "cpu": "arm64" }, "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], @@ -1125,7 +1199,7 @@ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], "@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="], @@ -1231,17 +1305,19 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.8", "", {}, "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="], - "@google/generative-ai": ["@google/generative-ai@0.24.0", "", {}, "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q=="], + "@google/genai": ["@google/genai@1.44.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-kRt9ZtuXmz+tLlcNntN/VV4LRdpl6ZOu5B1KbfNgfR65db15O6sUQcwnwLka8sT/V6qysD93fWrgJHF2L7dA9A=="], - "@googleapis/youtube": ["@googleapis/youtube@20.0.0", "", { "dependencies": { "googleapis-common": "^7.0.0" } }, "sha512-wdt1J0JoKYhvpoS2XIRHX0g/9ul/B0fQeeJAhuuBIdYINuuLt6/oZYZZCBmkuhtkA3IllXgqgAXOjLtLRAnR2g=="], + "@google/generative-ai": ["@google/generative-ai@0.24.0", "", {}, "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.9.15", "", { "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" } }, "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + "@happy-dom/jest-environment": ["@happy-dom/jest-environment@20.8.3", "", { "dependencies": { "happy-dom": "^20.8.3" }, "peerDependencies": { "@jest/environment": ">=25.0.0", "@jest/fake-timers": ">=25.0.0", "@jest/types": ">=25.0.0", "jest-mock": ">=25.0.0", "jest-util": ">=25.0.0" } }, "sha512-VMOfNvF7UPPHIc7SUrFqGXqJrkONYX6Vd0ZXblmjgb1JA2RFnrc1KiVodzG0c7IT5Q0jfA0CQjvlqWjQ/BYtkQ=="], + "@headlessui/react": ["@headlessui/react@2.2.4", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA=="], - "@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -1251,6 +1327,10 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -1315,7 +1395,7 @@ "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], + "@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], "@jest/get-type": ["@jest/get-type@30.1.0", "", {}, "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA=="], @@ -1359,7 +1439,7 @@ "@langchain/aws": ["@langchain/aws@0.1.15", "", { "dependencies": { "@aws-sdk/client-bedrock-agent-runtime": "^3.755.0", "@aws-sdk/client-bedrock-runtime": "^3.840.0", "@aws-sdk/client-kendra": "^3.750.0", "@aws-sdk/credential-provider-node": "^3.750.0" }, "peerDependencies": { "@langchain/core": ">=0.3.58 <0.4.0" } }, "sha512-oyOMhTHP0rxdSCVI/g5KXYCOs9Kq/FpXMZbOk1JSIUoaIzUg4p6d98lsHu7erW//8NSaT+SX09QRbVDAgt7pNA=="], - "@langchain/core": ["@langchain/core@0.3.79", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.32", "zod-to-json-schema": "^3.22.3" } }, "sha512-ZLAs5YMM5N2UXN3kExMglltJrKKoW7hs3KMZFlXUnD7a5DFKBYxPFMeXA4rT+uvTxuJRZPCYX0JKI5BhyAWx4A=="], + "@langchain/core": ["@langchain/core@0.3.80", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.32", "zod-to-json-schema": "^3.22.3" } }, "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA=="], "@langchain/deepseek": ["@langchain/deepseek@0.0.2", "", { "dependencies": { "@langchain/openai": "^0.5.5" }, "peerDependencies": { "@langchain/core": ">=0.3.58 <0.4.0" } }, "sha512-u13KbPUXW7uhcybbRzYdRroBgqVUSgG0SJM15c7Etld2yjRQC2c4O/ga9eQZdLh/kaDlQfH/ZITFdjHe77RnGw=="], @@ -1405,7 +1485,7 @@ "@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="], - "@librechat/agents": ["@librechat/agents@3.0.50", "", { "dependencies": { "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.79", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "axios": "^1.12.1", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "openai": "5.8.2" } }, "sha512-oovj3BsP/QoxPbWFAc71Ddplwd9BT8ucfYs+n+kiR37aCWtvxdvL9/XldRYfnaq9boNE324njQJyqc8v8AAPFQ=="], + "@librechat/agents": ["@librechat/agents@3.1.55", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.980.0", "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.80", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "@scarf/scarf": "^1.4.0", "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "okapibm25": "^1.4.1", "openai": "5.8.2" } }, "sha512-impxeKpCDlPkAVQFWnA6u6xkxDSBR/+H8uYq7rZomBeu0rUh/OhJLiI1fAwPhKXP33udNtHA8GyDi0QJj78R9w=="], "@librechat/api": ["@librechat/api@workspace:packages/api"], @@ -1421,14 +1501,44 @@ "@mcp-ui/client": ["@mcp-ui/client@5.7.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "*", "@quilted/threads": "^3.1.3", "@r2wc/react-to-web-component": "^2.0.4", "@remote-dom/core": "^1.8.0", "@remote-dom/react": "^1.2.2", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-+HbPw3VS46WUSWmyJ34ZVnygb81QByA3luR6y0JDbyDZxjYtHw1FcIN7v9WbbE8PrfI0WcuWCSiNOO6sOGbwpQ=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.0.1", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ=="], + "@microsoft/microsoft-graph-client": ["@microsoft/microsoft-graph-client@3.0.7", "", { "dependencies": { "@babel/runtime": "^7.12.5", "tslib": "^2.2.0" } }, "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw=="], "@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.0", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-z0Zhn/LmQ3yz91dEfd5QgS7DpSjA4pk+3z2++zKgn5L6iDFM9QapsVoAQSbKLvlrFsZk9+ru6yHHWNq2lCYJKQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], + + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.1", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg=="], + "@napi-rs/canvas": ["@napi-rs/canvas@0.1.96", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.96", "@napi-rs/canvas-darwin-arm64": "0.1.96", "@napi-rs/canvas-darwin-x64": "0.1.96", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.96", "@napi-rs/canvas-linux-arm64-gnu": "0.1.96", "@napi-rs/canvas-linux-arm64-musl": "0.1.96", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-musl": "0.1.96", "@napi-rs/canvas-win32-arm64-msvc": "0.1.96", "@napi-rs/canvas-win32-x64-msvc": "0.1.96" } }, "sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew=="], + + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.96", "", { "os": "android", "cpu": "arm64" }, "sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg=="], + + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.96", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg=="], + + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.96", "", { "os": "darwin", "cpu": "x64" }, "sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA=="], + + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.96", "", { "os": "linux", "cpu": "arm" }, "sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ=="], + + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw=="], + + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg=="], + + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.96", "", { "os": "linux", "cpu": "none" }, "sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig=="], + + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg=="], + + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ=="], + + "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.96", "", { "os": "win32", "cpu": "arm64" }, "sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA=="], + + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.96", "", { "os": "win32", "cpu": "x64" }, "sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -1479,11 +1589,11 @@ "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="], - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], @@ -1547,7 +1657,7 @@ "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A=="], - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dialog": "1.0.2", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0MtxV53FaEEBOKRgyLnEqHZKKDS5BldQ9oUBsKVXWI5FHbl2jp35qs+0aJET+K5hJDsc40kQUzP7g+wC7tqrqA=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA=="], @@ -1561,17 +1671,17 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dismissable-layer": "1.0.2", "@radix-ui/react-focus-guards": "1.0.0", "@radix-ui/react-focus-scope": "1.0.1", "@radix-ui/react-id": "1.0.0", "@radix-ui/react-portal": "1.0.1", "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-slot": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-EKxxp2WNSmUPkx4trtWNmZ4/vAYEg7JkAfa1HKBUnaubw9eHzf1Orr9B472lJYaYz327RHDrd4R95fsw7VR8DA=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-escape-keydown": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg=="], "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ=="], "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.0.7", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.5", "@radix-ui/react-popper": "1.1.3", "@radix-ui/react-portal": "1.0.4", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A=="], @@ -1587,7 +1697,7 @@ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1", "@radix-ui/react-use-rect": "1.0.1", "@radix-ui/react-use-size": "1.0.1", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], @@ -1619,7 +1729,7 @@ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], @@ -1673,7 +1783,7 @@ "@redis/client": ["@redis/client@1.6.0", "", { "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", "yallist": "4.0.0" } }, "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg=="], - "@remix-run/router": ["@remix-run/router@1.15.0", "", {}, "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ=="], + "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], "@remote-dom/core": ["@remote-dom/core@1.9.0", "", { "dependencies": { "@remote-dom/polyfill": "^1.4.4", "htm": "^3.1.1" }, "peerDependencies": { "@preact/signals-core": "^1.3.0" } }, "sha512-h8OO2NRns2paXO/q5hkfXrwlZKq7oKj9XedGosi7J8OP3+aW7N2Gv4MBBVVQGCfOiZPkOj5m3sQH7FdyUWl7PQ=="], @@ -1681,6 +1791,8 @@ "@remote-dom/react": ["@remote-dom/react@1.2.2", "", { "dependencies": { "@remote-dom/core": "^1.7.0", "@types/react": "^18.0.0", "htm": "^3.1.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0" } }, "sha512-PkvioODONTr1M0StGDYsR4Ssf5M0Rd4+IlWVvVoK3Zrw8nr7+5mJkgNofaj/z7i8Aep78L28PCW8/WduUt4unA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + "@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.0", "", { "dependencies": { "slash": "^4.0.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, "sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ=="], "@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" } }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="], @@ -1721,10 +1833,18 @@ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA=="], "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.37.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw=="], "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA=="], @@ -1735,121 +1855,129 @@ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.37.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg=="], "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.37.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], - "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw=="], + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], - "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.0.0", "", { "dependencies": { "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig=="], + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg=="], - "@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], + "@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@3.2.0", "", { "dependencies": { "@smithy/node-config-provider": "^3.1.4", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "tslib": "^2.6.2" } }, "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA=="], - "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-OZfsI+YRG26XZik/jKMMg37acnBSbUiK/8nETW3uM3mLj+0tMmFXdHQw1e5WEd/IHN8BGOh3te91SNDe2o4RHg=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.0.1", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.0.0", "@smithy/chunked-blob-reader-native": "^4.0.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-rkFIrQOKZGS6i1D3gKJ8skJ0RlXqDvb1IyAphksaFOMzkn3v3I1eJ8m7OkLj0jf1McP63rcCEoLlkAn/HjcTRw=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.12", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w=="], + "@smithy/hash-node": ["@smithy/hash-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-U1rAE1fxmReCIr6D2o/4ROqAQX+GffZpyMt3d7njtGDr2pUNmAKRWa49gsNVhCh2vVAuf3wXzWwNr2YN8PAXIw=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], - "@smithy/md5-js": ["@smithy/md5-js@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLZ647L27APi6zXkZlzSFZIjpo8po45YiyjMGJZM3gyDY8n7dPGdmxIIljLm4gPt/7rRvutLTTkYJpZVfG5r+A=="], + "@smithy/md5-js": ["@smithy/md5-js@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.0.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", "@smithy/node-config-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.0.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/service-error-classification": "^4.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" } }, "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.40", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/service-error-classification": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.0.1", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.6", "", { "dependencies": { "@smithy/abort-controller": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], "@smithy/property-provider": ["@smithy/property-provider@3.1.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg=="], + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw=="], + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0" } }, "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA=="], + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.1.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-stack": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" } }, "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], - "@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], - "@smithy/url-parser": ["@smithy/url-parser@4.0.1", "", { "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g=="], + "@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], - "@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], + "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w=="], + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.0.7", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.39", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.0.7", "", { "dependencies": { "@smithy/config-resolver": "^4.0.1", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.42", "", { "dependencies": { "@smithy/config-resolver": "^4.4.10", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA=="], - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], - "@smithy/util-retry": ["@smithy/util-retry@4.0.1", "", { "dependencies": { "@smithy/service-error-classification": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw=="], + "@smithy/util-retry": ["@smithy/util-retry@4.2.11", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw=="], - "@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], - "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], - "@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.0.6", "", { "dependencies": { "@smithy/abort-controller": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.11", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A=="], - "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], "@stitches/core": ["@stitches/core@1.2.8", "", {}, "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg=="], @@ -1883,10 +2011,6 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], - - "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="], - "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], @@ -1913,9 +2037,69 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - "@types/diff": ["@types/diff@6.0.0", "", {}, "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], @@ -1927,6 +2111,8 @@ "@types/express-session": ["@types/express-session@1.18.2", "", { "dependencies": { "@types/express": "*" } }, "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], @@ -2011,10 +2197,14 @@ "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], "@types/winston": ["@types/winston@2.4.4", "", { "dependencies": { "winston": "*" } }, "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@types/xml-encryption": ["@types/xml-encryption@1.2.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q=="], "@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="], @@ -2039,6 +2229,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.4", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], @@ -2079,48 +2271,14 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - "@webassemblyjs/ast": ["@webassemblyjs/ast@1.12.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg=="], - - "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.11.6", "", {}, "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="], - - "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.11.6", "", {}, "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="], - - "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.12.1", "", {}, "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="], - - "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.11.6", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g=="], - - "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.11.6", "", {}, "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="], - - "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/wasm-gen": "1.12.1" } }, "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g=="], - - "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.11.6", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg=="], - - "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.11.6", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ=="], - - "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.11.6", "", {}, "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="], - - "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/helper-wasm-section": "1.12.1", "@webassemblyjs/wasm-gen": "1.12.1", "@webassemblyjs/wasm-opt": "1.12.1", "@webassemblyjs/wasm-parser": "1.12.1", "@webassemblyjs/wast-printer": "1.12.1" } }, "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g=="], - - "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", "@webassemblyjs/utf8": "1.11.6" } }, "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w=="], - - "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/wasm-gen": "1.12.1", "@webassemblyjs/wasm-parser": "1.12.1" } }, "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg=="], - - "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", "@webassemblyjs/utf8": "1.11.6" } }, "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ=="], - - "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.10", "", {}, "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="], - "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], - - "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - - "abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], - "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], @@ -2129,8 +2287,6 @@ "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "acorn-globals": ["acorn-globals@7.0.1", "", { "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" } }, "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q=="], - "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -2139,12 +2295,10 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], - "anser": ["anser@2.1.1", "", {}, "sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ=="], "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], @@ -2161,7 +2315,7 @@ "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], @@ -2223,11 +2377,11 @@ "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@30.2.0", "", { "dependencies": { "@types/babel__core": "^7.20.5" } }, "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA=="], - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.12", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og=="], + "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.11.1", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ=="], + "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.3", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q=="], + "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], "babel-plugin-replace-ts-export-assignment": ["babel-plugin-replace-ts-export-assignment@0.0.2", "", {}, "sha512-BiTEG2Ro+O1spuheL5nB289y37FFmz0ISE6GjpNCG2JuA/WNcuEHSYw01+vN8quGf208sID3FnZFDwVyqX18YQ=="], @@ -2253,7 +2407,7 @@ "base64url": ["base64url@3.0.1", "", {}, "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="], @@ -2261,9 +2415,11 @@ "binary-extensions": ["binary-extensions@2.2.0", "", {}, "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="], + "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], + "bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -2351,9 +2507,11 @@ "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], + "chevrotain": ["chevrotain@11.1.2", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", "@chevrotain/regexp-to-ast": "11.1.2", "@chevrotain/types": "11.1.2", "@chevrotain/utils": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg=="], - "chrome-trace-event": ["chrome-trace-event@1.0.3", "", {}, "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="], + "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], + + "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -2419,6 +2577,8 @@ "concat-with-sourcemaps": ["concat-with-sourcemaps@1.1.0", "", { "dependencies": { "source-map": "^0.6.1" } }, "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "connect-redis": ["connect-redis@8.1.0", "", { "peerDependencies": { "express-session": ">=1" } }, "sha512-Km0EYLDlmExF52UCss5gLGTtrukGC57G6WCC2aqEMft5Vr4xNWuM4tL+T97kWrw+vp40SXFteb6Xk/7MxgpwdA=="], "console-browserify": ["console-browserify@1.2.0", "", {}, "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="], @@ -2445,13 +2605,13 @@ "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="], - "core-js-compat": ["core-js-compat@3.40.0", "", { "dependencies": { "browserslist": "^4.24.3" } }, "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ=="], + "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" } }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], "create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="], @@ -2473,13 +2633,13 @@ "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], - "css-blank-pseudo": ["css-blank-pseudo@5.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-aCU4AZ7uEcVSUzagTlA9pHciz7aWPKA/YzrEkpdSopJ2pvhIxiQ5sYeMz1/KByxlIo4XBdvMNJAVKMg/GRnhfw=="], + "css-blank-pseudo": ["css-blank-pseudo@8.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-C5B2e5hCM4llrQkUms+KnWEMVW8K1n2XvX9G7ppfMZJQ7KAS/4rNnkP1Cs+HhWriOz1mWWTMFD4j1J7s31Dgug=="], "css-declaration-sorter": ["css-declaration-sorter@6.4.1", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g=="], - "css-has-pseudo": ["css-has-pseudo@5.0.2", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.1", "postcss-selector-parser": "^6.0.10", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-q+U+4QdwwB7T9VEW/LyO6CFrLAeLqOykC5mDqJXc7aKZAhDbq7BvGT13VGJe+IwBfdN2o3Xdw2kJ5IxwV1Sc9Q=="], + "css-has-pseudo": ["css-has-pseudo@8.0.0", "", { "dependencies": { "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Uz/bsHRbOeir/5Oeuz85tq/yLJLxX+3dpoRdjNTshs6jjqwUg8XaEZGDd0ci3fw7l53Srw0EkJ8mYan0eW5uGQ=="], - "css-prefers-color-scheme": ["css-prefers-color-scheme@8.0.2", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-OvFghizHJ45x7nsJJUSYLyQNTzsCU8yWjxAc/nhPQg1pbs18LMoET8N3kOweFDPy0JV0OSXN2iqRFhPBHYOeMA=="], + "css-prefers-color-scheme": ["css-prefers-color-scheme@11.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-fv0mgtwUhh2m9iio3Kxc2CkrogjIaRdMFaaqyzSFdii17JF4cfPyMNX72B15ZW2Nrr/NZUpxI4dec1VMHYJvdw=="], "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], @@ -2489,7 +2649,7 @@ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], - "cssdb": ["cssdb@7.10.0", "", {}, "sha512-yGZ5tmA57gWh/uvdQBHs45wwFY0IBh3ypABk5sEubPBPSzXzkNgsWReqx7gdx6uhC+QoFBe+V8JwBB9/hQ6cIA=="], + "cssdb": ["cssdb@8.8.0", "", {}, "sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q=="], "cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -2503,14 +2663,84 @@ "csso": ["csso@4.2.0", "", { "dependencies": { "css-tree": "^1.1.2" } }, "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA=="], - "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], - "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], @@ -2527,7 +2757,7 @@ "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -2553,6 +2783,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -2577,12 +2809,14 @@ "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], - "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], + "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], "diffie-hellman": ["diffie-hellman@5.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg=="], + "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], "dnd-core": ["dnd-core@16.0.1", "", { "dependencies": { "@react-dnd/asap": "^5.0.1", "@react-dnd/invariant": "^4.0.1", "redux": "^4.2.0" } }, "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng=="], @@ -2599,11 +2833,9 @@ "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - "domexception": ["domexception@4.0.0", "", { "dependencies": { "webidl-conversions": "^7.0.0" } }, "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw=="], - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="], + "dompurify": ["dompurify@3.3.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -2611,6 +2843,8 @@ "downloadjs": ["downloadjs@1.4.7", "", {}, "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q=="], + "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -2639,7 +2873,7 @@ "enhanced-resolve": ["enhanced-resolve@5.17.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -2655,8 +2889,6 @@ "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], - "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], - "es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -2671,7 +2903,7 @@ "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], - "esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": "bin/esbuild" }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -2683,8 +2915,6 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", "esgenerate": "bin/esgenerate.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], "eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "build/bin/cli.js" }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="], @@ -2755,11 +2985,11 @@ "export-from-json": ["export-from-json@1.7.4", "", {}, "sha512-FjmpluvZS2PTYyhkoMfQoyEJMfe2bfAyNpa5Apa6C9n7SWUWyJkG/VFnzERuj3q9Jjo3iwBjwVsDQ7Z7sczthA=="], - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-mongo-sanitize": ["express-mongo-sanitize@2.2.0", "", {}, "sha512-PZBs5nwhD6ek9ZuP+W2xmpvcrHwXZxD5GdieX2dsjPbAbH4azOkrHbycBud2QRU+YQF1CT+pki/lZGedHgo/dQ=="], - "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], + "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], "express-session": ["express-session@1.18.2", "", { "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", "on-headers": "~1.1.0", "parseurl": "~1.3.3", "safe-buffer": "5.2.1", "uid-safe": "~2.1.5" } }, "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A=="], @@ -2787,7 +3017,7 @@ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], + "fast-xml-parser": ["fast-xml-parser@5.3.8", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-53jIF4N6u/pxvaL1eb/hEZts/cFLWZ92eCfLrNyCI0k38lettCG/Bs40W9pPwoPXyHQlKu2OUbQtiEIZK/J6Vw=="], "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="], @@ -2849,7 +3079,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "framer-motion": ["framer-motion@12.23.9", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ=="], @@ -2867,7 +3097,7 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - "gaxios": ["gaxios@5.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA=="], + "gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], "gcp-metadata": ["gcp-metadata@5.3.0", "", { "dependencies": { "gaxios": "^5.0.0", "json-bigint": "^1.0.0" } }, "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w=="], @@ -2897,12 +3127,10 @@ "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], - "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], - "globals": ["globals@15.14.0", "", {}, "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -2911,8 +3139,6 @@ "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], - "googleapis-common": ["googleapis-common@7.0.1", "", { "dependencies": { "extend": "^3.0.2", "gaxios": "^6.0.3", "google-auth-library": "^9.0.0", "qs": "^6.7.0", "url-template": "^2.0.8", "uuid": "^9.0.0" } }, "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -2921,10 +3147,14 @@ "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "hamt_plus": ["hamt_plus@1.0.2", "", {}, "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="], "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": "bin/handlebars" }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + "happy-dom": ["happy-dom@20.8.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ=="], + "harmony-reflect": ["harmony-reflect@1.6.2", "", {}, "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g=="], "has-bigints": ["has-bigints@1.0.2", "", {}, "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ=="], @@ -2973,7 +3203,7 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="], + "hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="], "hookified": ["hookified@1.12.1", "", {}, "sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q=="], @@ -3025,6 +3255,8 @@ "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "import-cwd": ["import-cwd@3.0.0", "", { "dependencies": { "import-from": "^3.0.0" } }, "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg=="], "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], @@ -3049,11 +3281,13 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "intersection-observer": ["intersection-observer@0.10.0", "", {}, "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ=="], "ioredis": ["ioredis@5.3.2", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA=="], - "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -3215,7 +3449,7 @@ "jest-message-util": ["jest-message-util@30.2.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw=="], - "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], + "jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" } }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], @@ -3231,7 +3465,7 @@ "jest-snapshot": ["jest-snapshot@30.2.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "@jest/snapshot-utils": "30.2.0", "@jest/transform": "30.2.0", "@jest/types": "30.2.0", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", "expect": "30.2.0", "graceful-fs": "^4.2.11", "jest-diff": "30.2.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-util": "30.2.0", "pretty-format": "30.2.0", "semver": "^7.7.2", "synckit": "^0.11.8" } }, "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA=="], - "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + "jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], "jest-validate": ["jest-validate@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.2.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", "pretty-format": "30.2.0" } }, "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw=="], @@ -3283,6 +3517,8 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="], "jwks-rsa": ["jwks-rsa@3.2.0", "", { "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww=="], @@ -3297,16 +3533,22 @@ "keyv-file": ["keyv-file@5.2.0", "", { "dependencies": { "@keyv/serialize": "^1.0.1", "tslib": "^1.14.1" } }, "sha512-5JEBqQiDzjGCQHtf7KLReJdHKchaJyUZW+9TvBu+4dc+uuTqUG9KcdA3ICMXlwky3qjKc0ecNCNefbgjyDtlAg=="], + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], - "langsmith": ["langsmith@0.3.67", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "p-retry": "4", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" } }, "sha512-l4y3RmJ9yWF5a29fLg3eWZQxn6Q6dxTOgLGgQHzPGZHF3NUynn+A+airYIe/Yt4rwjGbuVrABAPsXBkVu/Hi7g=="], + "langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="], + + "langsmith": ["langsmith@0.4.12", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-YWt0jcGvKqjUgIvd78rd4QcdMss0lUkeUaqp0UpVRq7H2yNDx8H5jOUO/laWUmaPtWGgcip0qturykXe1g9Gqw=="], "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "ldap-filter": ["ldap-filter@0.3.3", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg=="], "ldapauth-fork": ["ldapauth-fork@5.0.5", "", { "dependencies": { "@types/ldapjs": "^2.2.2", "bcryptjs": "^2.4.0", "ldapjs": "^2.2.1", "lru-cache": "^7.10.1" } }, "sha512-LWUk76+V4AOZbny/3HIPQtGPWZyA3SW2tRhsWIBi9imP22WJktKLHV1ofd8Jo/wY7Ve6vAT7FCI5mEn3blZTjw=="], @@ -3319,6 +3561,8 @@ "librechat-data-provider": ["librechat-data-provider@workspace:packages/data-provider"], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "limiter": ["limiter@1.1.5", "", {}, "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="], @@ -3329,13 +3573,13 @@ "listr2": ["listr2@8.2.5", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ=="], - "loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="], - "loader-utils": ["loader-utils@3.3.1", "", {}, "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -3367,8 +3611,6 @@ "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], - "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], - "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], @@ -3381,6 +3623,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="], + "lowlight": ["lowlight@2.9.0", "", { "dependencies": { "@types/hast": "^2.0.0", "fault": "^2.0.0", "highlight.js": "~11.8.0" } }, "sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw=="], "lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], @@ -3399,8 +3643,12 @@ "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + "mammoth": ["mammoth@1.11.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "match-sorter": ["match-sorter@8.1.0", "", { "dependencies": { "@babel/runtime": "^7.23.8", "remove-accents": "0.5.0" } }, "sha512-0HX3BHPixkbECX+Vt7nS1vJ6P2twPgGTU3PMXjWrl1eyVCL24tFHeyYN1FN5RKLzve0TyzNI9qntqQGbebnfPQ=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -3459,6 +3707,8 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "mermaid": ["mermaid@11.13.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], "micromark": ["micromark@4.0.0", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ=="], @@ -3543,20 +3793,24 @@ "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "mkdirp": ["mkdirp@1.0.4", "", { "bin": "bin/cmd.js" }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="], + "module-alias": ["module-alias@2.2.3", "", {}, "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q=="], "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "mongodb": ["mongodb@6.14.2", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@mongodb-js/zstd", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q=="], "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], @@ -3579,7 +3833,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "multer": ["multer@2.0.2", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "mkdirp": "^0.5.6", "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" } }, "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw=="], + "multer": ["multer@2.1.1", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "type-is": "^1.6.18" } }, "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A=="], "mustache": ["mustache@4.2.0", "", { "bin": "bin/mustache" }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], @@ -3605,6 +3859,8 @@ "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + "node-readable-to-web-readable-stream": ["node-readable-to-web-readable-stream@0.4.2", "", {}, "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "node-stdlib-browser": ["node-stdlib-browser@1.3.1", "", { "dependencies": { "assert": "^2.0.0", "browser-resolve": "^2.0.0", "browserify-zlib": "^0.2.0", "buffer": "^5.7.1", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", "create-require": "^1.1.1", "crypto-browserify": "^3.12.1", "domain-browser": "4.22.0", "events": "^3.0.0", "https-browserify": "^1.0.0", "isomorphic-timers-promises": "^1.0.1", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "pkg-dir": "^5.0.0", "process": "^0.11.10", "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.0.0", "timers-browserify": "^2.0.4", "tty-browserify": "0.0.1", "url": "^0.11.4", "util": "^0.12.4", "vm-browserify": "^1.0.1" } }, "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw=="], @@ -3651,6 +3907,8 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "okapibm25": ["okapibm25@1.4.1", "", {}, "sha512-UHmeH4MAtZXGFVncwbY7pfFvDVNxpsyM3W66aGPU0SHj1+ld59ty+9lJ0ifcrcnPUl1XdYoDgb06ObyCnpTs3g=="], + "ollama": ["ollama@0.5.18", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -3671,6 +3929,8 @@ "openid-client": ["openid-client@6.5.0", "", { "dependencies": { "jose": "^6.0.10", "oauth4webapi": "^3.5.1" } }, "sha512-fAfYaTnOYE2kQCqEJGX9KDObW2aw7IQy4jWpU/+3D3WoCFLbix5Hg6qIPQ6Js9r7f8jDUmsnnguRNCSw4wU/IQ=="], + "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="], + "optionator": ["optionator@0.9.3", "", { "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0" } }, "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg=="], "os-browserify": ["os-browserify@0.3.0", "", {}, "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="], @@ -3695,6 +3955,8 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -3737,6 +3999,8 @@ "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -3745,16 +4009,18 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pause": ["pause@0.0.1", "", {}, "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="], "pbkdf2": ["pbkdf2@3.1.3", "", { "dependencies": { "create-hash": "~1.1.3", "create-hmac": "^1.1.7", "ripemd160": "=2.0.1", "safe-buffer": "^5.2.1", "sha.js": "^2.4.11", "to-buffer": "^1.2.0" } }, "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA=="], + "pdfjs-dist": ["pdfjs-dist@5.5.207", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.95", "node-readable-to-web-readable-stream": "^0.4.2" } }, "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw=="], + "peek-readable": ["peek-readable@5.0.0", "", {}, "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -3773,37 +4039,43 @@ "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": "cli.js" }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="], "playwright-core": ["playwright-core@1.56.1", "", { "bin": "cli.js" }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], - "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@6.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IRuCwwAAQbgaLhxQdQcIIK0dCVXg3XDUnzgKD8iwdiYdwU4rMWRWyl/W9/0nA4ihVpq5pyALiHB2veBJ0292pw=="], + "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@8.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fovIPEV35c2JzVXdmP+sp2xirbBMt54J+upU8u6TSj410kUU5+axgEzvBBSAX8KCybze8CFCelzFAw/FfWg2TA=="], "postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="], "postcss-clamp": ["postcss-clamp@4.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.6" } }, "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow=="], - "postcss-color-functional-notation": ["postcss-color-functional-notation@5.1.0", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^2.3.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-w2R4py6zrVE1U7FwNaAc76tNQlG9GLkrBbcFw+VhUjyDDiV28vfZG+l4LyPmpoQpeSJVtu8VgNjE8Jv5SpC7dQ=="], + "postcss-color-functional-notation": ["postcss-color-functional-notation@8.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-tbmkk6teYpJzFcGwPIhN1gkvxqGHvNx2PMb8Y3S5Ktyn7xOlvD98XzQ99MFY5mAyvXWclDG+BgoJKYJXFJOp5Q=="], - "postcss-color-hex-alpha": ["postcss-color-hex-alpha@9.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-7sEHU4tAS6htlxun8AB9LDrCXoljxaC34tFVRlYKcvO+18r5fvGiXgv5bQzN40+4gXLCyWSMRK5FK31244WcCA=="], + "postcss-color-hex-alpha": ["postcss-color-hex-alpha@11.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-NCGa6vjIyrjosz9GqRxVKbONBklz5TeipYqTJp3IqbnBWlBq5e5EMtG6MaX4vqk9LzocPfMQkuRK9tfk+OQuKg=="], - "postcss-color-rebeccapurple": ["postcss-color-rebeccapurple@8.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-xWf/JmAxVoB5bltHpXk+uGRoGFwu4WDAR7210el+iyvTdqiKpDhtcT8N3edXMoVJY0WHFMrKMUieql/wRNiXkw=="], + "postcss-color-rebeccapurple": ["postcss-color-rebeccapurple@11.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-g9561mx7cbdqx7XeO/L+lJzVlzu7bICyXr72efBVKZGxIhvBBJf9fGXn3Cb6U4Bwh3LbzQO2e9NWBLVYdX5Eag=="], "postcss-colormin": ["postcss-colormin@5.3.1", "", { "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0", "colord": "^2.9.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ=="], "postcss-convert-values": ["postcss-convert-values@5.1.3", "", { "dependencies": { "browserslist": "^4.21.4", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA=="], - "postcss-custom-media": ["postcss-custom-media@9.1.5", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^1.0.2", "@csstools/css-parser-algorithms": "^2.2.0", "@csstools/css-tokenizer": "^2.1.1", "@csstools/media-query-list-parser": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-GStyWMz7Qbo/Gtw1xVspzVSX8eipgNg4lpsO3CAeY4/A1mzok+RV6MCv3fg62trWijh/lYEj6vps4o8JcBBpDA=="], + "postcss-custom-media": ["postcss-custom-media@12.0.1", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-66syE14+VeqkUf0rRX0bvbTCbNRJF132jD+ceo8th1dap2YJEAqpdh5uG98CE3IbgHT7m9XM0GIlOazNWqQdeA=="], - "postcss-custom-properties": ["postcss-custom-properties@13.3.4", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^1.0.7", "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9YN0gg9sG3OH+Z9xBrp2PWRb+O4msw+5Sbp3ZgqrblrwKspXVQe5zr5sVqi43gJGwW/Rv1A483PRQUzQOEewvA=="], + "postcss-custom-properties": ["postcss-custom-properties@15.0.1", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-cuyq8sd8dLY0GLbelz1KB8IMIoDECo6RVXMeHeXY2Uw3Q05k/d1GVITdaKLsheqrHbnxlwxzSRZQQ5u+rNtbMg=="], - "postcss-custom-selectors": ["postcss-custom-selectors@7.1.6", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^1.0.5", "@csstools/css-parser-algorithms": "^2.3.2", "@csstools/css-tokenizer": "^2.2.1", "postcss-selector-parser": "^6.0.13" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-svsjWRaxqL3vAzv71dV0/65P24/FB8TbPX+lWyyf9SZ7aZm4S4NhCn7N3Bg+Z5sZunG3FS8xQ80LrCU9hb37cw=="], + "postcss-custom-selectors": ["postcss-custom-selectors@9.0.1", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-2XBELy4DmdVKimChfaZ2id9u9CSGYQhiJ53SvlfBvMTzLMW2VxuMb9rHsMSQw9kRq/zSbhT5x13EaK8JSmK8KQ=="], - "postcss-dir-pseudo-class": ["postcss-dir-pseudo-class@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-cMnslilYxBf9k3qejnovrUONZx1rXeUZJw06fgIUBzABJe3D2LiLL5WAER7Imt3nrkaIgG05XZBztueLEf5P8w=="], + "postcss-dir-pseudo-class": ["postcss-dir-pseudo-class@10.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-DmtIzULpyC8XaH4b5AaUgt4Jic4QmrECqidNCdR7u7naQFdnxX80YI06u238a+ZVRXwURDxVzy0s/UQnWmpVeg=="], "postcss-discard-comments": ["postcss-discard-comments@5.1.2", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ=="], @@ -3813,31 +4085,27 @@ "postcss-discard-overridden": ["postcss-discard-overridden@5.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw=="], - "postcss-double-position-gradients": ["postcss-double-position-gradients@4.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^2.3.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-nUAbUXURemLXIrl4Xoia2tiu5z/n8sY+BVDZApoeT9BlpByyrp02P/lFCRrRvZ/zrGRE+MOGLhk8o7VcMCtPtQ=="], + "postcss-double-position-gradients": ["postcss-double-position-gradients@7.0.0", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Msr/dxj8Os7KLJE5Hdhvprwm3K5Zrh1KTY0eFN3ngPKNkej/Usy4BM9JQmqE6CLAkDpHoQVsi4snbL72CPt6qg=="], - "postcss-focus-visible": ["postcss-focus-visible@8.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-f/Vd+EC/GaKElknU59esVcRYr/Y3t1ZAQyL4u2xSOgkDy4bMCmG7VP5cGvj3+BTLNE9ETfEuz2nnt4qkZwTTeA=="], + "postcss-focus-visible": ["postcss-focus-visible@11.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-VG1a9kBKizUBWS66t5xyB4uLONBnvZLCmZXxT40FALu8EF0QgVZBYy5ApC0KhmpHsv+pvHMJHB3agKHwmocWjw=="], - "postcss-focus-within": ["postcss-focus-within@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AHAJ89UQBcqBvFgQJE9XasGuwMNkKsGj4D/f9Uk60jFmEBHpAL14DrnSk3Rj+SwZTr/WUG+mh+Rvf8fid/346w=="], + "postcss-focus-within": ["postcss-focus-within@10.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-dvql0fzUTG+gcJYp+KTbag5vAjuo94LDYZHkqDV1rnf5gPGer1v/SrmIZBdvKU8moep3HbcbujqGjzSb3DL53Q=="], "postcss-font-variant": ["postcss-font-variant@5.0.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA=="], - "postcss-gap-properties": ["postcss-gap-properties@4.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-V5OuQGw4lBumPlwHWk/PRfMKjaq/LTGR4WDTemIMCaMevArVfCCA9wBJiL1VjDAd+rzuCIlkRoRvDsSiAaZ4Fg=="], + "postcss-gap-properties": ["postcss-gap-properties@7.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-PSDF2QoZMRUbsINvXObQgxx4HExRP85QTT8qS/YN9fBsCPWCqUuwqAD6E6PNp0BqL/jU1eyWUBORaOK/J/9LDA=="], - "postcss-image-set-function": ["postcss-image-set-function@5.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Sszjwo0ubETX0Fi5MvpYzsONwrsjeabjMoc5YqHvURFItXgIu3HdCjcVuVKGMPGzKRhgaknmdM5uVWInWPJmeg=="], + "postcss-image-set-function": ["postcss-image-set-function@8.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rEGNkOkNusf4+IuMmfEoIdLuVmvbExGbmG+MIsyV6jR5UaWSoyPcAYHV/PxzVDCmudyF+2Nh/o6Ub2saqUdnuA=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], - "postcss-initial": ["postcss-initial@4.0.1", "", { "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ=="], - "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="], - "postcss-lab-function": ["postcss-lab-function@5.2.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fi32AYKzji5/rvgxo5zXHFvAYBw0u0OzELbeCNjEZVLUir18Oj+9RmNphtM8QdLUaUnrfx8zy8vVYLmFLkdmrQ=="], + "postcss-lab-function": ["postcss-lab-function@8.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1ZIAh8ODhZdnAb09Aq2BTenePKS1G/kUR0FwvzkQDfFtSOV64Ycv27YvV11fDycEvhIcEmgYkLABXKRiWcXRuA=="], "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" } }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], - "postcss-loader": ["postcss-loader@7.3.4", "", { "dependencies": { "cosmiconfig": "^8.3.5", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A=="], - - "postcss-logical": ["postcss-logical@6.2.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-aqlfKGaY0nnbgI9jwUikp4gJKBqcH5noU/EdnIVceghaaDPYhZuyJVxlvWNy55tlTG5tunRKCTAX9yljLiFgmw=="], + "postcss-logical": ["postcss-logical@9.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-A4LNd9dk3q/juEUA9Gd8ALhBO3TeOeYurnyHLlf2aAToD94VHR8c5Uv7KNmf8YVRhTxvWsyug4c5fKtARzyIRQ=="], "postcss-merge-longhand": ["postcss-merge-longhand@5.1.7", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^5.1.1" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ=="], @@ -3863,7 +4131,7 @@ "postcss-nested": ["postcss-nested@6.0.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.11" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ=="], - "postcss-nesting": ["postcss-nesting@11.3.0", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-JlS10AQm/RzyrUGgl5irVkAlZYTJ99mNueUl+Qab+TcHhVedLiylWVkKBhRale+rS9yWIJK48JVzQlq3LcSdeA=="], + "postcss-nesting": ["postcss-nesting@14.0.0", "", { "dependencies": { "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw=="], "postcss-normalize-charset": ["postcss-normalize-charset@5.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg=="], @@ -3883,19 +4151,19 @@ "postcss-normalize-whitespace": ["postcss-normalize-whitespace@5.1.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA=="], - "postcss-opacity-percentage": ["postcss-opacity-percentage@2.0.0", "", { "peerDependencies": { "postcss": "^8.2" } }, "sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ=="], + "postcss-opacity-percentage": ["postcss-opacity-percentage@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ=="], "postcss-ordered-values": ["postcss-ordered-values@5.1.3", "", { "dependencies": { "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ=="], - "postcss-overflow-shorthand": ["postcss-overflow-shorthand@4.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-HQZ0qi/9iSYHW4w3ogNqVNr2J49DHJAl7r8O2p0Meip38jsdnRPgiDW7r/LlLrrMBMe3KHkvNtAV2UmRVxzLIg=="], + "postcss-overflow-shorthand": ["postcss-overflow-shorthand@7.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9SLpjoUdGRoRrzoOdX66HbUs0+uDwfIAiXsRa7piKGOqPd6F4ZlON9oaDSP5r1Qpgmzw5L9Ht0undIK6igJPMA=="], "postcss-page-break": ["postcss-page-break@3.0.4", "", { "peerDependencies": { "postcss": "^8" } }, "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ=="], - "postcss-place": ["postcss-place@8.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Ow2LedN8sL4pq8ubukO77phSVt4QyCm35ZGCYXKvRFayAwcpgB0sjNJglDoTuRdUL32q/ZC1VkPBo0AOEr4Uiw=="], + "postcss-place": ["postcss-place@11.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fAifpyjQ+fuDRp2nmF95WbotqbpjdazebedahXdfBxy5sHembOLpBQ1cHveZD9ZmjK26tYM8tikeNaUlp/KfHA=="], - "postcss-preset-env": ["postcss-preset-env@8.5.1", "", { "dependencies": { "@csstools/postcss-cascade-layers": "^3.0.1", "@csstools/postcss-color-function": "^2.2.3", "@csstools/postcss-color-mix-function": "^1.0.3", "@csstools/postcss-font-format-keywords": "^2.0.2", "@csstools/postcss-gradients-interpolation-method": "^3.0.6", "@csstools/postcss-hwb-function": "^2.2.2", "@csstools/postcss-ic-unit": "^2.0.4", "@csstools/postcss-is-pseudo-class": "^3.2.1", "@csstools/postcss-logical-float-and-clear": "^1.0.1", "@csstools/postcss-logical-resize": "^1.0.1", "@csstools/postcss-logical-viewport-units": "^1.0.3", "@csstools/postcss-media-minmax": "^1.0.4", "@csstools/postcss-media-queries-aspect-ratio-number-values": "^1.0.4", "@csstools/postcss-nested-calc": "^2.0.2", "@csstools/postcss-normalize-display-values": "^2.0.1", "@csstools/postcss-oklab-function": "^2.2.3", "@csstools/postcss-progressive-custom-properties": "^2.3.0", "@csstools/postcss-relative-color-syntax": "^1.0.2", "@csstools/postcss-scope-pseudo-class": "^2.0.2", "@csstools/postcss-stepped-value-functions": "^2.1.1", "@csstools/postcss-text-decoration-shorthand": "^2.2.4", "@csstools/postcss-trigonometric-functions": "^2.1.1", "@csstools/postcss-unset-value": "^2.0.1", "autoprefixer": "^10.4.14", "browserslist": "^4.21.9", "css-blank-pseudo": "^5.0.2", "css-has-pseudo": "^5.0.2", "css-prefers-color-scheme": "^8.0.2", "cssdb": "^7.6.0", "postcss-attribute-case-insensitive": "^6.0.2", "postcss-clamp": "^4.1.0", "postcss-color-functional-notation": "^5.1.0", "postcss-color-hex-alpha": "^9.0.2", "postcss-color-rebeccapurple": "^8.0.2", "postcss-custom-media": "^9.1.5", "postcss-custom-properties": "^13.2.0", "postcss-custom-selectors": "^7.1.3", "postcss-dir-pseudo-class": "^7.0.2", "postcss-double-position-gradients": "^4.0.4", "postcss-focus-visible": "^8.0.2", "postcss-focus-within": "^7.0.2", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^4.0.1", "postcss-image-set-function": "^5.0.2", "postcss-initial": "^4.0.1", "postcss-lab-function": "^5.2.3", "postcss-logical": "^6.2.0", "postcss-nesting": "^11.3.0", "postcss-opacity-percentage": "^2.0.0", "postcss-overflow-shorthand": "^4.0.1", "postcss-page-break": "^3.0.4", "postcss-place": "^8.0.1", "postcss-pseudo-class-any-link": "^8.0.2", "postcss-replace-overflow-wrap": "^4.0.0", "postcss-selector-not": "^7.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-qhWnJJjP6ArLUINWJ38t6Aftxnv9NW6cXK0NuwcLCcRilbuw72dSFLkCVUJeCfHGgJiKzX+pnhkGiki0PEynWg=="], + "postcss-preset-env": ["postcss-preset-env@11.2.0", "", { "dependencies": { "@csstools/postcss-alpha-function": "^2.0.3", "@csstools/postcss-cascade-layers": "^6.0.0", "@csstools/postcss-color-function": "^5.0.2", "@csstools/postcss-color-function-display-p3-linear": "^2.0.2", "@csstools/postcss-color-mix-function": "^4.0.2", "@csstools/postcss-color-mix-variadic-function-arguments": "^2.0.2", "@csstools/postcss-content-alt-text": "^3.0.0", "@csstools/postcss-contrast-color-function": "^3.0.2", "@csstools/postcss-exponential-functions": "^3.0.1", "@csstools/postcss-font-format-keywords": "^5.0.0", "@csstools/postcss-font-width-property": "^1.0.0", "@csstools/postcss-gamut-mapping": "^3.0.2", "@csstools/postcss-gradients-interpolation-method": "^6.0.2", "@csstools/postcss-hwb-function": "^5.0.2", "@csstools/postcss-ic-unit": "^5.0.0", "@csstools/postcss-initial": "^3.0.0", "@csstools/postcss-is-pseudo-class": "^6.0.0", "@csstools/postcss-light-dark-function": "^3.0.0", "@csstools/postcss-logical-float-and-clear": "^4.0.0", "@csstools/postcss-logical-overflow": "^3.0.0", "@csstools/postcss-logical-overscroll-behavior": "^3.0.0", "@csstools/postcss-logical-resize": "^4.0.0", "@csstools/postcss-logical-viewport-units": "^4.0.0", "@csstools/postcss-media-minmax": "^3.0.1", "@csstools/postcss-media-queries-aspect-ratio-number-values": "^4.0.0", "@csstools/postcss-mixins": "^1.0.0", "@csstools/postcss-nested-calc": "^5.0.0", "@csstools/postcss-normalize-display-values": "^5.0.1", "@csstools/postcss-oklab-function": "^5.0.2", "@csstools/postcss-position-area-property": "^2.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/postcss-property-rule-prelude-list": "^2.0.0", "@csstools/postcss-random-function": "^3.0.1", "@csstools/postcss-relative-color-syntax": "^4.0.2", "@csstools/postcss-scope-pseudo-class": "^5.0.0", "@csstools/postcss-sign-functions": "^2.0.1", "@csstools/postcss-stepped-value-functions": "^5.0.1", "@csstools/postcss-syntax-descriptor-syntax-production": "^2.0.0", "@csstools/postcss-system-ui-font-family": "^2.0.0", "@csstools/postcss-text-decoration-shorthand": "^5.0.3", "@csstools/postcss-trigonometric-functions": "^5.0.1", "@csstools/postcss-unset-value": "^5.0.0", "autoprefixer": "^10.4.24", "browserslist": "^4.28.1", "css-blank-pseudo": "^8.0.1", "css-has-pseudo": "^8.0.0", "css-prefers-color-scheme": "^11.0.0", "cssdb": "^8.8.0", "postcss-attribute-case-insensitive": "^8.0.0", "postcss-clamp": "^4.1.0", "postcss-color-functional-notation": "^8.0.2", "postcss-color-hex-alpha": "^11.0.0", "postcss-color-rebeccapurple": "^11.0.0", "postcss-custom-media": "^12.0.1", "postcss-custom-properties": "^15.0.1", "postcss-custom-selectors": "^9.0.1", "postcss-dir-pseudo-class": "^10.0.0", "postcss-double-position-gradients": "^7.0.0", "postcss-focus-visible": "^11.0.0", "postcss-focus-within": "^10.0.0", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^7.0.0", "postcss-image-set-function": "^8.0.0", "postcss-lab-function": "^8.0.2", "postcss-logical": "^9.0.0", "postcss-nesting": "^14.0.0", "postcss-opacity-percentage": "^3.0.0", "postcss-overflow-shorthand": "^7.0.0", "postcss-page-break": "^3.0.4", "postcss-place": "^11.0.0", "postcss-pseudo-class-any-link": "^11.0.0", "postcss-replace-overflow-wrap": "^4.0.0", "postcss-selector-not": "^9.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-eNYpuj68cjGjvZMoSAbHilaCt3yIyzBL1cVuSGJfvJewsaBW/U6dI2bqCJl3iuZsL+yvBobcy4zJFA/3I68IHQ=="], - "postcss-pseudo-class-any-link": ["postcss-pseudo-class-any-link@8.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FYTIuRE07jZ2CW8POvctRgArQJ43yxhr5vLmImdKUvjFCkR09kh8pIdlCwdx/jbFm7MiW4QP58L4oOUv3grQYA=="], + "postcss-pseudo-class-any-link": ["postcss-pseudo-class-any-link@11.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-DNFZ4GMa3C3pU5dM+UCTG1CEeLtS1ZqV5DKSqCTJQMn1G5jnd/30fS8+A7H4o5bSD3MOcnx+VgI+xPE9Z5Wvig=="], "postcss-reduce-initial": ["postcss-reduce-initial@5.1.2", "", { "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg=="], @@ -3903,7 +4171,7 @@ "postcss-replace-overflow-wrap": ["postcss-replace-overflow-wrap@4.0.0", "", { "peerDependencies": { "postcss": "^8.0.3" } }, "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw=="], - "postcss-selector-not": ["postcss-selector-not@7.0.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1zT5C27b/zeJhchN7fP0kBr16Cc61mu7Si9uWWLoA3Px/D9tIJPKchJCkUH3tPO5D0pCFmGeApAv8XpXBQJ8SQ=="], + "postcss-selector-not": ["postcss-selector-not@9.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-xhAtTdHnVU2M/CrpYOPyRUvg3njhVlKmn2GNYXDaRJV9Ygx4d5OkSkc7NINzjUqnbDFtaKXlISOBeyMXU/zyFQ=="], "postcss-selector-parser": ["postcss-selector-parser@6.0.15", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw=="], @@ -3937,7 +4205,7 @@ "property-information": ["property-information@6.4.1", "", {}, "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w=="], - "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -3945,8 +4213,6 @@ "pseudomap": ["pseudomap@1.0.2", "", {}, "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="], - "psl": ["psl@1.9.0", "", {}, "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="], - "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], "public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="], @@ -3961,8 +4227,6 @@ "querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="], - "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "random-bytes": ["random-bytes@1.0.0", "", {}, "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="], @@ -4003,23 +4267,21 @@ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "react-lazy-load-image-component": ["react-lazy-load-image-component@1.6.0", "", { "dependencies": { "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1" }, "peerDependencies": { "react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", "react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x" } }, "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ=="], - "react-lifecycles-compat": ["react-lifecycles-compat@3.0.4", "", {}, "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="], "react-markdown": ["react-markdown@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg=="], - "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + "react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-resizable-panels": ["react-resizable-panels@3.0.6", "", { "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew=="], - "react-router": ["react-router@6.22.0", "", { "dependencies": { "@remix-run/router": "1.15.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg=="], + "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], - "react-router-dom": ["react-router-dom@6.22.0", "", { "dependencies": { "@remix-run/router": "1.15.0", "react-router": "6.22.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag=="], + "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], "react-speech-recognition": ["react-speech-recognition@3.10.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-EVSr4Ik8l9urwdPiK2r0+ADrLyDDrjB0qBRdUWO+w2MfwEBrj6NuRmy1GD3x7BU/V6/hab0pl8Lupen0zwlJyw=="], @@ -4057,8 +4319,6 @@ "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], - "regenerator-transform": ["regenerator-transform@0.15.2", "", { "dependencies": { "@babel/runtime": "^7.8.4" } }, "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg=="], - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "regexpu-core": ["regexpu-core@6.2.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA=="], @@ -4097,8 +4357,6 @@ "requireindex": ["requireindex@1.1.0", "", {}, "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg=="], - "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], - "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], @@ -4115,10 +4373,12 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rimraf": ["rimraf@6.1.2", "", { "dependencies": { "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g=="], + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], "ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], + "rollup": ["rollup@4.37.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="], "rollup-plugin-peer-deps-external": ["rollup-plugin-peer-deps-external@2.2.4", "", { "peerDependencies": { "rollup": "*" } }, "sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g=="], @@ -4129,6 +4389,8 @@ "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], @@ -4137,6 +4399,8 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -4157,15 +4421,13 @@ "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], - "seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], "semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + "serialize-javascript": ["serialize-javascript@7.0.4", "", {}, "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg=="], "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], @@ -4237,6 +4499,8 @@ "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "static-browser-server": ["static-browser-server@1.0.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.1.0", "dotenv": "^16.0.3", "mime-db": "^1.52.0", "outvariant": "^1.3.0" } }, "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA=="], "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -4297,7 +4561,7 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "strnum": ["strnum@1.0.5", "", {}, "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="], + "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], "strtok3": ["strtok3@7.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.0.0" } }, "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ=="], @@ -4309,6 +4573,8 @@ "stylehacks": ["stylehacks@5.1.1", "", { "dependencies": { "browserslist": "^4.21.4", "postcss-selector-parser": "^6.0.4" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw=="], + "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], "superagent": ["superagent@9.0.2", "", { "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", "formidable": "^3.5.1", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0" } }, "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w=="], @@ -4321,7 +4587,9 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svgo": ["svgo@2.8.0", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", "stable": "^0.1.8" }, "bin": "bin/svgo" }, "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg=="], + "svgo": ["svgo@2.8.2", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": "./bin/svgo" }, "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA=="], + + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], @@ -4349,8 +4617,6 @@ "terser": ["terser@5.27.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": "bin/terser" }, "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A=="], - "terser-webpack-plugin": ["terser-webpack-plugin@5.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", "terser": "^5.26.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w=="], - "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], @@ -4367,7 +4633,9 @@ "tiny-emitter": ["tiny-emitter@2.1.0", "", {}, "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="], - "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], @@ -4403,8 +4671,12 @@ "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "ts-md5": ["ts-md5@1.3.1", "", {}, "sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg=="], + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], @@ -4413,6 +4685,20 @@ "tty-browserify": ["tty-browserify@0.0.1", "", {}, "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="], + "turbo": ["turbo@2.8.14", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.14", "turbo-darwin-arm64": "2.8.14", "turbo-linux-64": "2.8.14", "turbo-linux-arm64": "2.8.14", "turbo-windows-64": "2.8.14", "turbo-windows-arm64": "2.8.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-UCTxeMNYT1cKaHiIFdLCQ7ulI+jw5i5uOnJOrRXsgUD7G3+OjlUjwVd7JfeVt2McWSVGjYA3EVW/v1FSsJ5DtA=="], + + "turbo-darwin-64": ["turbo-darwin-64@2.8.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-9sFi7n2lLfEsGWi5OEoA/eTtQU2BPKtzSYKqufMtDeRmqMT9vKjbv9gJCRkllSVE9BOXA0qXC3diyX8V8rKIKw=="], + + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aS4yJuy6A1PCLws+PJpZP0qCURG8Y5iVx13z/WAbKyeDTY6W6PiGgcEllSaeLGxyn++382ztN/EZH85n2zZ6VQ=="], + + "turbo-linux-64": ["turbo-linux-64@2.8.14", "", { "os": "linux", "cpu": "x64" }, "sha512-XC6wPUDJkakjhNLaS0NrHDMiujRVjH+naEAwvKLArgqRaFkNxjmyNDRM4eu3soMMFmjym6NTxYaF74rvET+Orw=="], + + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ChfE7isyVNjZrVSPDwcfqcHLG/FuIBbOFxnt1FM8vSuBGzHAs8AlTdwFNIxlEMJfZ8Ad9mdMxdmsCUPIWiQ6cg=="], + + "turbo-windows-64": ["turbo-windows-64@2.8.14", "", { "os": "win32", "cpu": "x64" }, "sha512-FTbIeQL1ycLFW2t9uQNMy+bRSzi3Xhwun/e7ZhFBdM+U0VZxxrtfYEBM9CHOejlfqomk6Jh7aRz0sJoqYn39Hg=="], + + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-KgZX12cTyhY030qS7ieT8zRkhZZE2VWJasDFVUSVVn17nR7IShpv68/7j5UqJNeRLIGF1XPK0phsP5V5yw3how=="], + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -4441,6 +4727,8 @@ "ua-parser-js": ["ua-parser-js@1.0.37", "", {}, "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ=="], + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "uglify-js": ["uglify-js@3.17.4", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g=="], "uid-safe": ["uid-safe@2.1.5", "", { "dependencies": { "random-bytes": "~1.0.0" } }, "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA=="], @@ -4451,7 +4739,9 @@ "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], - "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], + "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], + + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -4495,10 +4785,6 @@ "url": ["url@0.11.4", "", { "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" } }, "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg=="], - "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], - - "url-template": ["url-template@2.0.8", "", {}, "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-composed-ref": ["use-composed-ref@1.3.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ=="], @@ -4535,36 +4821,42 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "tsx"], "bin": "bin/vite.js" }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite-plugin-compression2": ["vite-plugin-compression2@2.2.1", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "tar-mini": "^0.2.0" } }, "sha512-LMDkgheJaFBmb8cB8ymgUpXHXnd3m4kmjEInvp59fOZMSaT/9oDUtqpO0ihr4ExGsnWfYcRe13/TNN3BEk2t/g=="], - "vite-plugin-node-polyfills": ["vite-plugin-node-polyfills@0.23.0", "", { "dependencies": { "@rollup/plugin-inject": "^5.0.5", "node-stdlib-browser": "^1.2.0" }, "peerDependencies": { "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, "sha512-4n+Ys+2bKHQohPBKigFlndwWQ5fFKwaGY6muNDMTb0fSQLyBzS+jjUNRZG9sKF0S/Go4ApG6LFnUGopjkILg3w=="], + "vite-plugin-node-polyfills": ["vite-plugin-node-polyfills@0.25.0", "", { "dependencies": { "@rollup/plugin-inject": "^5.0.5", "node-stdlib-browser": "^1.3.1" }, "peerDependencies": { "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg=="], - "vite-plugin-pwa": ["vite-plugin-pwa@0.21.2", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^0.2.6", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg=="], + "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], - "watchpack": ["watchpack@2.4.2", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "webpack": ["webpack@5.94.0", "", { "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": "bin/webpack.js" }, "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg=="], - - "webpack-sources": ["webpack-sources@3.2.3", "", {}, "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="], - "websocket-driver": ["websocket-driver@0.7.4", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="], "websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="], @@ -4595,37 +4887,37 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "workbox-background-sync": ["workbox-background-sync@7.3.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.3.0" } }, "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg=="], + "workbox-background-sync": ["workbox-background-sync@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w=="], - "workbox-broadcast-update": ["workbox-broadcast-update@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA=="], + "workbox-broadcast-update": ["workbox-broadcast-update@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA=="], - "workbox-build": ["workbox-build@7.3.0", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^7.1.6", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.43.1", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "7.3.0", "workbox-broadcast-update": "7.3.0", "workbox-cacheable-response": "7.3.0", "workbox-core": "7.3.0", "workbox-expiration": "7.3.0", "workbox-google-analytics": "7.3.0", "workbox-navigation-preload": "7.3.0", "workbox-precaching": "7.3.0", "workbox-range-requests": "7.3.0", "workbox-recipes": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0", "workbox-streams": "7.3.0", "workbox-sw": "7.3.0", "workbox-window": "7.3.0" } }, "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ=="], + "workbox-build": ["workbox-build@7.4.0", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^11.0.1", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.79.2", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "7.4.0", "workbox-broadcast-update": "7.4.0", "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-google-analytics": "7.4.0", "workbox-navigation-preload": "7.4.0", "workbox-precaching": "7.4.0", "workbox-range-requests": "7.4.0", "workbox-recipes": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0", "workbox-streams": "7.4.0", "workbox-sw": "7.4.0", "workbox-window": "7.4.0" } }, "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA=="], - "workbox-cacheable-response": ["workbox-cacheable-response@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA=="], + "workbox-cacheable-response": ["workbox-cacheable-response@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ=="], - "workbox-core": ["workbox-core@7.3.0", "", {}, "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw=="], + "workbox-core": ["workbox-core@7.4.0", "", {}, "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ=="], - "workbox-expiration": ["workbox-expiration@7.3.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.3.0" } }, "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ=="], + "workbox-expiration": ["workbox-expiration@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw=="], - "workbox-google-analytics": ["workbox-google-analytics@7.3.0", "", { "dependencies": { "workbox-background-sync": "7.3.0", "workbox-core": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0" } }, "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg=="], + "workbox-google-analytics": ["workbox-google-analytics@7.4.0", "", { "dependencies": { "workbox-background-sync": "7.4.0", "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ=="], - "workbox-navigation-preload": ["workbox-navigation-preload@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg=="], + "workbox-navigation-preload": ["workbox-navigation-preload@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w=="], - "workbox-precaching": ["workbox-precaching@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0" } }, "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw=="], + "workbox-precaching": ["workbox-precaching@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg=="], - "workbox-range-requests": ["workbox-range-requests@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ=="], + "workbox-range-requests": ["workbox-range-requests@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw=="], - "workbox-recipes": ["workbox-recipes@7.3.0", "", { "dependencies": { "workbox-cacheable-response": "7.3.0", "workbox-core": "7.3.0", "workbox-expiration": "7.3.0", "workbox-precaching": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0" } }, "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg=="], + "workbox-recipes": ["workbox-recipes@7.4.0", "", { "dependencies": { "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-precaching": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ=="], - "workbox-routing": ["workbox-routing@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A=="], + "workbox-routing": ["workbox-routing@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ=="], - "workbox-strategies": ["workbox-strategies@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg=="], + "workbox-strategies": ["workbox-strategies@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg=="], - "workbox-streams": ["workbox-streams@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0", "workbox-routing": "7.3.0" } }, "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw=="], + "workbox-streams": ["workbox-streams@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0" } }, "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg=="], - "workbox-sw": ["workbox-sw@7.3.0", "", {}, "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA=="], + "workbox-sw": ["workbox-sw@7.4.0", "", {}, "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw=="], - "workbox-window": ["workbox-window@7.3.0", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.3.0" } }, "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA=="], + "workbox-window": ["workbox-window@7.4.0", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.4.0" } }, "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], @@ -4637,6 +4929,8 @@ "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "xlsx": ["xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }], + "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], "xml-crypto": ["xml-crypto@6.1.2", "", { "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", "xpath": "^0.0.33" } }, "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w=="], @@ -4647,7 +4941,7 @@ "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="], "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], @@ -4671,11 +4965,9 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "youtube-transcript": ["youtube-transcript@1.2.1", "", {}, "sha512-TvEGkBaajKw+B6y91ziLuBLsa5cawgowou+Bk0ciGpjELDfAzSzTGXaZmeSSkUeknCPpEr/WGApOHDwV7V+Y9Q=="], - "zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="], - "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -4735,6 +5027,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/core": ["@smithy/core@3.17.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-stream": "^4.5.5", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/hash-node": ["@smithy/hash-node@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw=="], @@ -4755,8 +5053,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -4771,88 +5073,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.952.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.947.0", "@aws-sdk/credential-provider-http": "3.947.0", "@aws-sdk/credential-provider-ini": "3.952.0", "@aws-sdk/credential-provider-process": "3.947.0", "@aws-sdk/credential-provider-sso": "3.952.0", "@aws-sdk/credential-provider-web-identity": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-pj7nidLrb3Dz9llcUPh6N0Yv1dBYTS9xJqi8u0kI8D5sn72HJMB+fIOhcDQVXXAw/dpVolOAH9FOAbog5JDAMg=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.948.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.7", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.947.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/config-resolver": ["@smithy/config-resolver@4.4.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-6OiaAaEbLB6dEkRbQyNzFSJv5HDvly3Mc6q/qcPd2uS/g3szR8wAIkh7UndAFKfMypNSTuZ6eCBmgCLR5LacTg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-xP5YXbOVRVN8A4pDnSUkEUsL9fYFU6VNhxo8tgr13YnMbf3Pn4xVr+hSyLVjS1Frfi1Uk03ET5Bwml4+0CeYEw=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-jhH7nJuaOpnTFcuZpWK9dqb6Ge2yGi1okTo0W6wkJrfwAm2vwmO74tF1v07JmrSyHBcKLQATEexclJw9K1Vj7w=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/hash-node": ["@smithy/hash-node@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/service-error-classification": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.15", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-LiZQVAg/oO8kueX4c+oMls5njaD2cRLXRfcjlTYjhIqmwHnCwkQO5B3dMQH0c5PACILxGAQf6Mxsq7CjlDc76A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.18", "", { "dependencies": { "@smithy/config-resolver": "^4.4.4", "@smithy/credential-provider-imds": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-retry": ["@smithy/util-retry@4.2.6", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.623.0", "", { "dependencies": { "@smithy/core": "^2.3.2", "@smithy/node-config-provider": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/signature-v4": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8Toq3X6trX/67obSdh4K0MFQY4f132bEbr1i0YPDWk/O3KdBt12mLC/sW3aVRnlIs110XMuX9yrWWqJ8fDW10g=="], "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.623.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-ini": "3.623.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qDwCOkhbu5PfaQHyuQ+h57HEx3+eFhKdtIw7aISziWkGdFrMe07yIBd7TJqGe4nxXnRF1pfkg05xeOlMId997g=="], @@ -4971,8 +5197,12 @@ "@aws-sdk/client-kendra/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@aws-sdk/client-kendra/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@aws-sdk/client-kendra/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], + "@aws-sdk/client-kendra/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@aws-sdk/client-kendra/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], "@aws-sdk/client-kendra/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -4987,11 +5217,13 @@ "@aws-sdk/client-kendra/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@aws-sdk/client-kendra/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@aws-sdk/client-kendra/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@aws-sdk/client-kendra/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/client-kendra/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], "@aws-sdk/client-sso/@aws-sdk/core": ["@aws-sdk/core@3.623.0", "", { "dependencies": { "@smithy/core": "^2.3.2", "@smithy/node-config-provider": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/signature-v4": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8Toq3X6trX/67obSdh4K0MFQY4f132bEbr1i0YPDWk/O3KdBt12mLC/sW3aVRnlIs110XMuX9yrWWqJ8fDW10g=="], @@ -5135,7 +5367,7 @@ "@aws-sdk/client-sso-oidc/@smithy/util-utf8": ["@smithy/util-utf8@3.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA=="], - "@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], + "@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.609.0", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q=="], @@ -5165,33 +5397,23 @@ "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], + "@aws-sdk/credential-provider-login/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA=="], - "@aws-sdk/credential-provider-login/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A=="], - "@aws-sdk/credential-provider-login/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-login": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ=="], - "@aws-sdk/credential-provider-login/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g=="], - "@aws-sdk/credential-provider-login/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-N27eFoRrO6MeUNumtNHDW9WOiwfd59LPXPqDrIa3kWL/s+fOKFHb9xIcF++bAwtcZnAxKkgpDCUP+INNZskE+w=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" } }, "sha512-Xt9/U8qUCiw1hihztWkNeIR+arg6P+yda10OuCHX6kFVx3auTlU7+hCqs3UxqniGU4dguHuftf3mRpi5/GJ33Q=="], + "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/credential-provider-env": "3.758.0", "@aws-sdk/credential-provider-http": "3.758.0", "@aws-sdk/credential-provider-process": "3.758.0", "@aws-sdk/credential-provider-sso": "3.758.0", "@aws-sdk/credential-provider-web-identity": "3.758.0", "@aws-sdk/nested-clients": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-cymSKMcP5d+OsgetoIZ5QCe1wnp2Q/tq+uIxVdh9MbfdBBEnl9Ecq6dH6VlYS89sp4QKuxHxkWXVnbXU3Q19Aw=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-AzcY74QTPqcbXWVgjpPZ3HOmxQZYPROIBz2YINF0OQk0MhezDWV/O7Xec+K1+MPGQO3qS6EDrUUlnPLjsqieHA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.758.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.758.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/token-providers": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-x0FYJqcOLUCv8GLLFDYMXRAQKGjoM+L0BG4BiHYZRDf24yQWFCAZsCQAYKo6XZYh2qznbsW6f//qpyJ5b0QVKQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/nested-clients": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-XGguXhBqiCXMXRxcfCAVPlMbm3VyJTou79r/3mxWddHWF0XbhaQiBIbUz6vobVTD25YQRbWSmSch7VA8kI5Lrw=="], - - "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg=="], - - "@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], + "@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.609.0", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q=="], @@ -5217,113 +5439,41 @@ "@aws-sdk/credential-providers/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@aws-sdk/eventstream-handler-node/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/middleware-websocket/@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA=="], - "@aws-sdk/eventstream-handler-node/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.758.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-0RPCo8fYJcrenJ6bRtiUbFOSgQ1CX/GpvwtLU2Fam1tS9h2klKK8d74caeV6A1mIUvBU7bhyQ0wMGlwMtn3EYw=="], - "@aws-sdk/middleware-eventstream/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], - "@aws-sdk/middleware-eventstream/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.0.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", "@smithy/node-config-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg=="], - "@aws-sdk/middleware-eventstream/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/s3-request-presigner/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], - "@aws-sdk/middleware-websocket/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client": ["@smithy/smithy-client@4.1.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-stack": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" } }, "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw=="], - "@aws-sdk/middleware-websocket/@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g=="], + "@aws-sdk/s3-request-presigner/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], - "@aws-sdk/middleware-websocket/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-6OiaAaEbLB6dEkRbQyNzFSJv5HDvly3Mc6q/qcPd2uS/g3szR8wAIkh7UndAFKfMypNSTuZ6eCBmgCLR5LacTg=="], + "@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/middleware-websocket/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/middleware-websocket/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.948.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.7", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA=="], - - "@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw=="], - - "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], - - "@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w=="], - - "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw=="], - - "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.947.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ=="], - - "@aws-sdk/nested-clients/@smithy/config-resolver": ["@smithy/config-resolver@4.4.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA=="], - - "@aws-sdk/nested-clients/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/nested-clients/@smithy/hash-node": ["@smithy/hash-node@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg=="], - - "@aws-sdk/nested-clients/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ=="], - - "@aws-sdk/nested-clients/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w=="], - - "@aws-sdk/nested-clients/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], - - "@aws-sdk/nested-clients/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/service-error-classification": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ=="], - - "@aws-sdk/nested-clients/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], - - "@aws-sdk/nested-clients/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], - - "@aws-sdk/nested-clients/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/nested-clients/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], - - "@aws-sdk/nested-clients/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/nested-clients/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], - - "@aws-sdk/nested-clients/@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.15", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-LiZQVAg/oO8kueX4c+oMls5njaD2cRLXRfcjlTYjhIqmwHnCwkQO5B3dMQH0c5PACILxGAQf6Mxsq7CjlDc76A=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.18", "", { "dependencies": { "@smithy/config-resolver": "^4.4.4", "@smithy/credential-provider-imds": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q=="], - - "@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg=="], - - "@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.6", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA=="], - - "@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], - - "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], - - "@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/token-providers/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/token-providers/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/util-format-url/@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], "@aws-sdk/util-format-url/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/util-format-url/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@azure/core-http-compat/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], - "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.0.9", "", { "dependencies": { "strnum": "^2.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A=="], + "@azure/storage-blob/@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], + + "@azure/storage-blob/@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="], + + "@azure/storage-blob/@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + + "@azure/storage-common/@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], + + "@azure/storage-common/@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + + "@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -5331,11 +5481,17 @@ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], - "@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.8", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg=="], @@ -5343,18 +5499,38 @@ "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.5.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - "@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA=="], + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "@codesandbox/sandpack-client/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "@csstools/css-color-parser/@csstools/color-helpers": ["@csstools/color-helpers@4.0.0", "", {}, "sha512-wjyXB22/h2OvxAr3jldPB7R7kjTUEzopvjitS8jWtyd8fN6xJ8vy1HnHu0ZNfEkqpBJgQ76Q+sBDshWcMvTa/w=="], + "@csstools/postcss-cascade-layers/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/postcss-is-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/postcss-scope-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/selector-resolve-nested/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/selector-specificity/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/config-array/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@google/genai/google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], + + "@grpc/proto-loader/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@headlessui/react/@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], @@ -5373,40 +5549,18 @@ "@istanbuljs/load-nyc-config/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "@jest/console/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/console/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@jest/core/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/core/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "@jest/core/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@jest/environment/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - - "@jest/environment/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "@jest/environment-jsdom-abstract/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - - "@jest/environment-jsdom-abstract/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "@jest/environment-jsdom-abstract/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/expect/expect": ["expect@30.2.0", "", { "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw=="], - "@jest/fake-timers/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@jest/fake-timers/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "@jest/globals/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - "@jest/reporters/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "@jest/reporters/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/reporters/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "@jest/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -5415,8 +5569,6 @@ "@jest/transform/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@jest/transform/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -5445,49 +5597,91 @@ "@langchain/mistralai/uuid": ["uuid@10.0.0", "", { "bin": "dist/bin/uuid" }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "@librechat/client/@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], + "@librechat/agents/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "@librechat/client/@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - - "@librechat/client/@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], - - "@librechat/frontend/@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], - - "@librechat/frontend/@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - - "@librechat/frontend/@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "@librechat/backend/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.6", "", { "dependencies": { "@smithy/abort-controller": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ=="], "@librechat/frontend/@react-spring/web": ["@react-spring/web@9.7.5", "", { "dependencies": { "@react-spring/animated": "~9.7.5", "@react-spring/core": "~9.7.5", "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ=="], "@librechat/frontend/@testing-library/jest-dom": ["@testing-library/jest-dom@5.17.0", "", { "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.5.6", "lodash": "^4.17.15", "redent": "^3.0.0" } }, "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg=="], - "@librechat/frontend/framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], + "@librechat/frontend/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "@librechat/frontend/jest-environment-jsdom": ["jest-environment-jsdom@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/jsdom": "^20.0.0", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0", "jsdom": "^20.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA=="], + "@librechat/frontend/dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="], + + "@librechat/frontend/framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], "@librechat/frontend/lucide-react": ["lucide-react@0.394.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw=="], "@mcp-ui/client/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.21.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" } }, "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ=="], + "@mistralai/mistralai/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], - - "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="], - "@node-saml/node-saml/@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + "@node-saml/node-saml/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@node-saml/node-saml/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "@node-saml/passport-saml/@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], "@node-saml/passport-saml/passport": ["passport@0.7.0", "", { "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", "utils-merge": "^1.0.1" } }, "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + + "@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], - "@radix-ui/react-alert-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-alert-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g=="], @@ -5503,11 +5697,29 @@ "@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], - "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], - "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], - "@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="], + + "@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw=="], + + "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w=="], + + "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.0", "", {}, "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="], @@ -5521,6 +5733,12 @@ "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-focus-scope/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + "@radix-ui/react-hover-card/@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], "@radix-ui/react-hover-card/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], @@ -5567,8 +5785,6 @@ "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], - "@radix-ui/react-menu/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], - "@radix-ui/react-popover/@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], "@radix-ui/react-popover/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], @@ -5591,8 +5807,6 @@ "@radix-ui/react-popover/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA=="], - "@radix-ui/react-popover/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], - "@radix-ui/react-popper/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg=="], @@ -5603,6 +5817,8 @@ "@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], + "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + "@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], @@ -5621,10 +5837,16 @@ "@radix-ui/react-select/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + "@radix-ui/react-select/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-select/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="], + "@radix-ui/react-select/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-select/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + "@radix-ui/react-select/react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + "@radix-ui/react-slider/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], "@radix-ui/react-slider/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], @@ -5669,6 +5891,8 @@ "@radix-ui/react-toast/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA=="], + "@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + "@radix-ui/react-use-size/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], "@rollup/plugin-babel/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], @@ -5681,45 +5905,21 @@ "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "@smithy/abort-controller/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - "@smithy/credential-provider-imds/@smithy/node-config-provider": ["@smithy/node-config-provider@3.1.4", "", { "dependencies": { "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ=="], "@smithy/credential-provider-imds/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], "@smithy/credential-provider-imds/@smithy/url-parser": ["@smithy/url-parser@3.0.3", "", { "dependencies": { "@smithy/querystring-parser": "^3.0.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A=="], - "@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], - - "@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - - "@smithy/middleware-retry/uuid": ["uuid@9.0.1", "", { "bin": "dist/bin/uuid" }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - - "@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - - "@smithy/node-http-handler/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@smithy/property-provider/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@smithy/querystring-builder/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], - "@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], - "@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - - "@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg=="], - - "@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - - "@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - - "@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA=="], + "@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], @@ -5737,12 +5937,22 @@ "@types/winston/winston": ["winston@3.11.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.4.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.5.0" } }, "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g=="], + "@types/ws/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + + "@typescript-eslint/parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@typescript-eslint/type-utils/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@typescript-eslint/typescript-estree/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + "@vitejs/plugin-react/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -5751,10 +5961,18 @@ "asn1.js/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "autoprefixer/fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "babel-plugin-root-import/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "body-parser/qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + + "body-parser/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "browser-resolve/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "browserify-rsa/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], @@ -5769,6 +5987,8 @@ "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "cheerio/undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -5783,32 +6003,66 @@ "cookie-parser/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + "core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "create-ecdh/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "css-blank-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "css-has-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "cssnano/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], "cssnano/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "data-urls/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], "diffie-hellman/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], - "domexception/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "eslint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-import-resolver-node/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "eslint-import-resolver-typescript/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-i18next/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-import/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-plugin-react/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "expect/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], + "expect/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + "express-session/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], "express-session/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -5819,25 +6073,25 @@ "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "finalhandler/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "flat-cache/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], - "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "gcp-metadata/gaxios": ["gaxios@5.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA=="], - "google-auth-library/gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], - "googleapis-common/gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], + "happy-dom/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "googleapis-common/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - "googleapis-common/uuid": ["uuid@9.0.1", "", { "bin": "dist/bin/uuid" }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - - "gtoken/gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], + "happy-dom/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "hast-util-from-html/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="], @@ -5871,10 +6125,16 @@ "http-proxy-agent/agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + "http-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "import-from/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@1.2.3", "", {}, "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="], + "ioredis/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "is-bun-module/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "istanbul-lib-instrument/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -5885,44 +6145,30 @@ "istanbul-lib-source-maps/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "istanbul-lib-source-maps/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "jake/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "jest-changed-files/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "jest-changed-files/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-circus/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], - "jest-circus/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-circus/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-circus/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-cli/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-config/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "jest-config/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-config/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-config/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "jest-diff/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "jest-each/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-each/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "jest-environment-node/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - - "jest-environment-node/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "jest-environment-node/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-file-loader/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-haste-map/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-leak-detector/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], @@ -5931,24 +6177,12 @@ "jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-mock/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-resolve/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-resolve/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-runner/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], - "jest-runtime/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - "jest-runtime/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "jest-runtime/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "jest-runtime/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-runtime/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], @@ -5959,28 +6193,18 @@ "jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], - "jest-snapshot/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-snapshot/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-snapshot/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "jest-snapshot/synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], - "jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "jest-validate/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "jest-watcher/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - - "jest-worker/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "jsdom/decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "jsdom/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], @@ -5991,30 +6215,30 @@ "jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "jwks-rsa/@types/express": ["@types/express@4.17.21", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ=="], + "jwks-rsa/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "jwks-rsa/jose": ["jose@4.15.5", "", {}, "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg=="], "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "keyv-file/@keyv/serialize": ["@keyv/serialize@1.0.3", "", { "dependencies": { "buffer": "^6.0.3" } }, "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g=="], - "keyv-file/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "langsmith/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "langsmith/uuid": ["uuid@10.0.0", "", { "bin": "dist/bin/uuid" }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], "ldapauth-fork/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "librechat-data-provider/@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], - - "librechat-data-provider/@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - - "librechat-data-provider/@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "librechat-data-provider/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "lint-staged/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "log-update/ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], @@ -6027,30 +6251,50 @@ "lru-memoizer/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "mathjs/fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mdast-util-math/unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "mdast-util-mdx-jsx/unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "memorystore/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "mermaid/dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "mermaid/uuid": ["uuid@11.1.0", "", { "bin": "dist/esm/bin/uuid" }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "micromark/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "miller-rabin/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + "mongodb-connection-string-url/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "mongodb-memory-server-core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "mongodb-memory-server-core/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "multer/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": "bin/cmd.js" }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "mquery/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "multer/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "new-find-package-json/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "node-stdlib-browser/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "node-stdlib-browser/pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="], + "nodemon/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "nodemon/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "nodemon/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -6073,18 +6317,26 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "postcss-attribute-case-insensitive/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-colormin/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], "postcss-convert-values/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "postcss-custom-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "postcss-dir-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "postcss-focus-visible/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "postcss-focus-within/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-import/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "postcss-load-config/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], "postcss-load-config/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], - "postcss-loader/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "postcss-merge-rules/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], "postcss-minify-params/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], @@ -6093,20 +6345,28 @@ "postcss-modules-scope/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], + "postcss-nesting/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-normalize-unicode/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - "postcss-preset-env/autoprefixer": ["autoprefixer@10.4.17", "", { "dependencies": { "browserslist": "^4.22.2", "caniuse-lite": "^1.0.30001578", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg=="], + "postcss-preset-env/autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], - "postcss-preset-env/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "postcss-preset-env/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "postcss-pseudo-class-any-link/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "postcss-reduce-initial/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "postcss-selector-not/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "protobufjs/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + "public-encrypt/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "rc-util/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -6133,6 +6393,8 @@ "remark-supersub/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + "require-in-the-middle/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -6147,7 +6409,9 @@ "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], - "schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "router/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "send/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "sharp/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -6173,6 +6437,8 @@ "sucrase/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "superagent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "superagent/mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], "superagent/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], @@ -6181,6 +6447,10 @@ "svgo/css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], + "svgo/sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + + "swr/use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], "tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], @@ -6193,17 +6463,9 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "terser-webpack-plugin/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "terser-webpack-plugin/jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "tinyglobby/fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], - - "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "ts-node/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], + "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], @@ -6225,19 +6487,19 @@ "vasync/verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], + "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "vfile-location/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], "vfile-location/vfile": ["vfile@5.3.7", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g=="], - "webpack/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], - "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - - "webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "vite/rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "winston-daily-rotate-file/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], - "workbox-build/@babel/preset-env": ["@babel/preset-env@7.23.9", "", { "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.23.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.23.3", "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.23.3", "@babel/plugin-transform-async-generator-functions": "^7.23.9", "@babel/plugin-transform-async-to-generator": "^7.23.3", "@babel/plugin-transform-block-scoped-functions": "^7.23.3", "@babel/plugin-transform-block-scoping": "^7.23.4", "@babel/plugin-transform-class-properties": "^7.23.3", "@babel/plugin-transform-class-static-block": "^7.23.4", "@babel/plugin-transform-classes": "^7.23.8", "@babel/plugin-transform-computed-properties": "^7.23.3", "@babel/plugin-transform-destructuring": "^7.23.3", "@babel/plugin-transform-dotall-regex": "^7.23.3", "@babel/plugin-transform-duplicate-keys": "^7.23.3", "@babel/plugin-transform-dynamic-import": "^7.23.4", "@babel/plugin-transform-exponentiation-operator": "^7.23.3", "@babel/plugin-transform-export-namespace-from": "^7.23.4", "@babel/plugin-transform-for-of": "^7.23.6", "@babel/plugin-transform-function-name": "^7.23.3", "@babel/plugin-transform-json-strings": "^7.23.4", "@babel/plugin-transform-literals": "^7.23.3", "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-amd": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@babel/plugin-transform-modules-systemjs": "^7.23.9", "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.23.3", "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", "@babel/plugin-transform-numeric-separator": "^7.23.4", "@babel/plugin-transform-object-rest-spread": "^7.23.4", "@babel/plugin-transform-object-super": "^7.23.3", "@babel/plugin-transform-optional-catch-binding": "^7.23.4", "@babel/plugin-transform-optional-chaining": "^7.23.4", "@babel/plugin-transform-parameters": "^7.23.3", "@babel/plugin-transform-private-methods": "^7.23.3", "@babel/plugin-transform-private-property-in-object": "^7.23.4", "@babel/plugin-transform-property-literals": "^7.23.3", "@babel/plugin-transform-regenerator": "^7.23.3", "@babel/plugin-transform-reserved-words": "^7.23.3", "@babel/plugin-transform-shorthand-properties": "^7.23.3", "@babel/plugin-transform-spread": "^7.23.3", "@babel/plugin-transform-sticky-regex": "^7.23.3", "@babel/plugin-transform-template-literals": "^7.23.3", "@babel/plugin-transform-typeof-symbol": "^7.23.3", "@babel/plugin-transform-unicode-escapes": "^7.23.3", "@babel/plugin-transform-unicode-property-regex": "^7.23.3", "@babel/plugin-transform-unicode-regex": "^7.23.3", "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.8", "babel-plugin-polyfill-corejs3": "^0.9.0", "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A=="], + "workbox-build/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], "workbox-build/@rollup/plugin-replace": ["@rollup/plugin-replace@2.4.2", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0" } }, "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg=="], @@ -6245,7 +6507,7 @@ "workbox-build/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - "workbox-build/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "workbox-build/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], @@ -6293,6 +6555,8 @@ "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw=="], "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ=="], @@ -6317,6 +6581,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], @@ -6325,6 +6595,8 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA=="], @@ -6349,62 +6621,6 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/credential-provider-env": "3.947.0", "@aws-sdk/credential-provider-http": "3.947.0", "@aws-sdk/credential-provider-login": "3.952.0", "@aws-sdk/credential-provider-process": "3.947.0", "@aws-sdk/credential-provider-sso": "3.952.0", "@aws-sdk/credential-provider-web-identity": "3.952.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-N5B15SwzMkZ8/LLopNksTlPEWWZn5tbafZAUfMY5Xde4rSHGWmv5H/ws2M3P8L0X77E2wKnOJsNmu+GsArBreQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.952.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.948.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/token-providers": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-1CQdP5RzxeXuEfytbAD5TgreY1c9OacjtCdO8+n9m05tpzBABoNBof0hcjzw1dtrWFH7deyUgfwCl1TAN3yBWQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5hJbfaZdHDAP8JlwplNbXJAat9Vv7L0AbTZzkbPIgjHhC3vrMf5r3a6I1HWFp5i5pXo7J45xyuf5uQGZJxJlCg=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/config-resolver/@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.6", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-olIfZ230B64TvPD6b0tPvrEp2eB0FkyL3KvDlqF4RVmIc/kn3orzXnV6DTQdOOW5UU+M5zKY3/BU47X420/oPw=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.6", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-olIfZ230B64TvPD6b0tPvrEp2eB0FkyL3KvDlqF4RVmIc/kn3orzXnV6DTQdOOW5UU+M5zKY3/BU47X420/oPw=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@4.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag=="], "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], @@ -6443,6 +6659,8 @@ "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw=="], "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ=="], @@ -6499,10 +6717,6 @@ "@aws-sdk/client-kendra/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - - "@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/client-sso-oidc/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@4.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag=="], "@aws-sdk/client-sso-oidc/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], @@ -6589,104 +6803,46 @@ "@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@3.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.758.0", "", { "dependencies": { "@aws-sdk/nested-clients": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-ckptN1tNrIfQUaGWm/ayW1ddG+imbKN7HHhjFdS4VfItsP0QQOB0+Ov+tpgb4MoNR4JaUghMIVStjIeHN2ks1w=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg=="], - "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], - "@aws-sdk/middleware-websocket/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.6", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-olIfZ230B64TvPD6b0tPvrEp2eB0FkyL3KvDlqF4RVmIc/kn3orzXnV6DTQdOOW5UU+M5zKY3/BU47X420/oPw=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-6mJ2zyyHPYSV6bAcaFpsdoXZJeQlR1QgBnZZ6juY/+dcYiuyWCdyLUbGzSZSE7GTfx6i+9+QWFeoIMlWKgU63A=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.0.1", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.0.1", "", { "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/nested-clients/@smithy/config-resolver/@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], - "@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA=="], - "@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], - "@aws-sdk/nested-clients/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], + "@aws-sdk/util-format-url/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/nested-clients/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/nested-clients/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], - - "@aws-sdk/nested-clients/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], - - "@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/nested-clients/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.0.5", "", {}, "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q=="], + "@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], @@ -6695,13 +6851,17 @@ "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], + "@google/genai/google-auth-library/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "@google/genai/google-auth-library/gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "@google/genai/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], "@headlessui/react/@tanstack/react-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], @@ -6711,34 +6871,16 @@ "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@jest/core/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "@jest/environment-jsdom-abstract/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - - "@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - - "@jest/environment/@jest/fake-timers/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - - "@jest/environment/jest-mock/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/expect/expect/@jest/expect-utils": ["@jest/expect-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0" } }, "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA=="], "@jest/expect/expect/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], - "@jest/expect/expect/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "@jest/expect/expect/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - - "@jest/fake-timers/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/fake-timers/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "@jest/globals/jest-mock/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/reporters/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@jest/reporters/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "@jest/reporters/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.927.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@aws-sdk/xml-builder": "3.921.0", "@smithy/core": "^3.17.2", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/signature-v4": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag=="], @@ -6773,6 +6915,12 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/core": ["@smithy/core@3.17.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-stream": "^4.5.5", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/hash-node": ["@smithy/hash-node@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw=="], @@ -6793,8 +6941,12 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -6809,12 +6961,16 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ=="], @@ -6835,275 +6991,21 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@langchain/google-gauth/google-auth-library/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "@langchain/google-gauth/google-auth-library/gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], "@langchain/google-gauth/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "@librechat/client/@babel/preset-env/core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "@librechat/frontend/@babel/preset-env/core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], "@librechat/frontend/@react-spring/web/@react-spring/animated": ["@react-spring/animated@9.7.5", "", { "dependencies": { "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg=="], @@ -7121,31 +7023,49 @@ "@librechat/frontend/@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@librechat/frontend/@testing-library/jest-dom/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "@librechat/frontend/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@librechat/frontend/framer-motion/motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], "@librechat/frontend/framer-motion/motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="], - "@librechat/frontend/jest-environment-jsdom/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "@librechat/frontend/jest-environment-jsdom/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@librechat/frontend/jest-environment-jsdom/@types/jsdom": ["@types/jsdom@20.0.1", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom": ["jsdom@20.0.3", "", { "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", "decimal.js": "^10.4.2", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.2", "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^4.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", "ws": "^8.11.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ=="], - "@mcp-ui/client/@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@node-saml/passport-saml/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], "@node-saml/passport-saml/@types/express/@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], @@ -7153,12 +7073,22 @@ "@radix-ui/react-checkbox/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], + "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@radix-ui/react-hover-card/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], "@radix-ui/react-hover-card/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg=="], @@ -7199,8 +7129,12 @@ "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], + "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + "@radix-ui/react-select/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], @@ -7237,26 +7171,42 @@ "@smithy/credential-provider-imds/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@3.0.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ=="], - "@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - - "@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@types/winston/winston/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], "@types/winston/winston/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], + "@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@vitejs/plugin-react/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@vitejs/plugin-react/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@vitejs/plugin-react/@babel/core/@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@vitejs/plugin-react/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@vitejs/plugin-react/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@vitejs/plugin-react/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "browserify-sign/readable-stream/core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "body-parser/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "browserify-sign/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "caniuse-api/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "caniuse-api/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -7271,14 +7221,32 @@ "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + + "core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "data-urls/whatwg-url/tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], "data-urls/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "eslint/@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "expect/jest-message-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], "expect/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "expect/jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "expect/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "expect/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "express-session/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "express-static-gzip/serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], @@ -7287,15 +7255,17 @@ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "gaxios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], - "google-auth-library/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], + "gaxios/https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "gcp-metadata/gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], - "googleapis-common/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], - - "gtoken/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], + "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "hast-util-from-html-isomorphic/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], @@ -7337,6 +7307,8 @@ "jest-config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "jest-config/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "jest-config/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "jest-config/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -7345,28 +7317,20 @@ "jest-each/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-environment-node/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - "jest-leak-detector/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-mock/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runtime/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - "jest-runtime/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "jest-runtime/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "jest-runtime/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "jest-snapshot/expect/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], + "jest-runtime/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "jest-snapshot/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-snapshot/synckit/@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], - "jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -7375,139 +7339,13 @@ "jsonwebtoken/jws/jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], + "jszip/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "jwks-rsa/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "librechat-data-provider/@babel/preset-env/core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], + "librechat-data-provider/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -7529,22 +7367,42 @@ "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "postcss-colormin/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-colormin/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-convert-values/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-convert-values/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-merge-rules/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-merge-rules/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-minify-params/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-minify-params/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-normalize-unicode/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-normalize-unicode/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - "postcss-preset-env/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-preset-env/autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], + + "postcss-preset-env/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], + + "postcss-preset-env/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], + + "postcss-preset-env/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "postcss-reduce-initial/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "postcss-reduce-initial/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "protobufjs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "rehype-highlight/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], "rehype-highlight/unified/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], @@ -7573,12 +7431,14 @@ "rollup-plugin-typescript2/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "stylehacks/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "stylehacks/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "sucrase/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "svgo/css-select/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], @@ -7587,135 +7447,75 @@ "tailwindcss/postcss-load-config/lilconfig": ["lilconfig@3.0.0", "", {}, "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g=="], - "terser-webpack-plugin/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "unist-util-remove-position/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], "unist-util-remove-position/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + "vasync/verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "vfile-location/vfile/unist-util-stringify-position": ["unist-util-stringify-position@3.0.3", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg=="], "vfile-location/vfile/vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="], - "webpack/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "vite/rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "winston-daily-rotate-file/winston-transport/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ=="], + "workbox-build/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ=="], + "workbox-build/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.7", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - "workbox-build/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw=="], + "workbox-build/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ=="], + "workbox-build/@babel/core/@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.23.9", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ=="], + "workbox-build/@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.23.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-remap-async-to-generator": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw=="], + "workbox-build/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A=="], + "workbox-build/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.23.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.23.4", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.23.8", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/template": "^7.22.15" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.23.3", "", { "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.23.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.23.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.23.9", "", { "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.22.5", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.23.4", "", { "dependencies": { "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.23.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.23.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "regenerator-transform": "^0.15.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.8", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.9.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.5.0", "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.5.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg=="], - - "workbox-build/@babel/preset-env/core-js-compat": ["core-js-compat@3.35.1", "", { "dependencies": { "browserslist": "^4.22.2" } }, "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw=="], + "workbox-build/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "workbox-build/@rollup/plugin-replace/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], @@ -7723,6 +7523,16 @@ "workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "workbox-build/glob/foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "workbox-build/glob/jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + + "workbox-build/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "workbox-build/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "workbox-build/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "workbox-build/source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], @@ -7735,7 +7545,11 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], @@ -7749,28 +7563,26 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.948.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.948.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], "@aws-sdk/client-cognito-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ=="], @@ -7791,7 +7603,11 @@ "@aws-sdk/client-cognito-identity/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], - "@aws-sdk/client-kendra/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], @@ -7805,10 +7621,18 @@ "@aws-sdk/client-kendra/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-kendra/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-kendra/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-kendra/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/client-kendra/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-kendra/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-kendra/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-kendra/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@aws-sdk/client-kendra/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -7873,92 +7697,126 @@ "@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.758.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.723.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.0.1", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-config-provider": ["@smithy/util-config-provider@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], - "@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - "@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], - "@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@google/genai/google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "@google/genai/google-auth-library/gaxios/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": "dist/esm/bin.mjs" }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@jest/expect/expect/jest-matcher-utils/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "@jest/fake-timers/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@jest/reporters/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], @@ -7967,6 +7825,8 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/eventstream-handler-node/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-recursion-detection/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.1.1", "", {}, "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA=="], @@ -7975,6 +7835,10 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], @@ -7983,6 +7847,10 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/config-resolver/@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], @@ -8013,6 +7881,8 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.927.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@aws-sdk/xml-builder": "3.921.0", "@smithy/core": "^3.17.2", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/signature-v4": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag=="], @@ -8023,6 +7893,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], @@ -8051,125 +7923,7 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": "dist/esm/bin.mjs" }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/client/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-jsx/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-pure-annotations/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/frontend/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-jsx/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-pure-annotations/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], "@librechat/frontend/@react-spring/web/@react-spring/shared/@react-spring/rafz": ["@react-spring/rafz@9.7.5", "", {}, "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw=="], @@ -8177,46 +7931,38 @@ "@librechat/frontend/@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@librechat/frontend/jest-environment-jsdom/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/cssstyle": ["cssstyle@2.3.0", "", { "dependencies": { "cssom": "~0.3.6" } }, "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/data-urls": ["data-urls@3.0.2", "", { "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0" } }, "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/nwsapi": ["nwsapi@2.2.7", "", {}, "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/tough-cookie": ["tough-cookie@4.1.3", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/w3c-xmlserializer": ["w3c-xmlserializer@4.0.0", "", { "dependencies": { "xml-name-validator": "^4.0.0" } }, "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-encoding": ["whatwg-encoding@2.0.0", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], - "@mcp-ui/client/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "@mcp-ui/client/@modelcontextprotocol/sdk/express/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@node-saml/passport-saml/@types/express/@types/express-serve-static-core/@types/qs": ["@types/qs@6.9.17", "", {}, "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ=="], + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], + "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], + "@vitejs/plugin-react/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -8225,6 +7971,8 @@ "expect/jest-message-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "expect/jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "express-static-gzip/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "express-static-gzip/serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -8233,11 +7981,11 @@ "express-static-gzip/serve-static/send/mime": ["mime@1.6.0", "", { "bin": "cli.js" }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "google-auth-library/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], + "gcp-metadata/gaxios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "googleapis-common/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], + "gcp-metadata/gaxios/https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "gtoken/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "jest-changed-files/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], @@ -8245,76 +7993,12 @@ "jest-config/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], - "jest-mock/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "jest-runtime/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "jest-runtime/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], - "jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "jsdom/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "librechat-data-provider/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-jsx/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-pure-annotations/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - "mongodb-connection-string-url/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "multer/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -8331,41 +8015,15 @@ "svgo/css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], - "terser-webpack-plugin/jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "workbox-build/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.22.20", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-wrap-function": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.22.20", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-wrap-function": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - - "workbox-build/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "workbox-build/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], "workbox-build/@rollup/plugin-replace/@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], @@ -8373,28 +8031,34 @@ "workbox-build/@rollup/plugin-replace/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "workbox-build/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + + "workbox-build/glob/path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "workbox-build/source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], "workbox-build/source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], - "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@aws-sdk/client-cognito-identity/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], - "@aws-sdk/client-kendra/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], "@aws-sdk/client-kendra/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -8409,76 +8073,86 @@ "@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@3.0.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "@google/genai/google-auth-library/gaxios/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "@jest/expect/expect/jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/eventstream-handler-node/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@aws-sdk/util-format-url/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -8491,10 +8165,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.921.0", "", { "dependencies": { "@smithy/types": "^4.8.1", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q=="], @@ -8503,8 +8183,12 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -8525,6 +8209,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.921.0", "", { "dependencies": { "@smithy/types": "^4.8.1", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q=="], @@ -8533,10 +8219,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.922.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA=="], @@ -8579,6 +8271,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -8595,6 +8289,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], @@ -8605,10 +8301,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.922.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA=="], @@ -8651,6 +8353,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -8667,6 +8371,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], @@ -8677,10 +8383,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], @@ -8691,10 +8403,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.922.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA=="], @@ -8737,6 +8455,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -8753,6 +8473,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], @@ -8761,387 +8483,33 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/client/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/frontend/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - "@librechat/frontend/@testing-library/jest-dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "@librechat/frontend/jest-environment-jsdom/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - "@librechat/frontend/jest-environment-jsdom/jsdom/cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - "@librechat/frontend/jest-environment-jsdom/jsdom/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "@librechat/frontend/jest-environment-jsdom/jsdom/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/tough-cookie/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-url/tr46": ["tr46@3.0.0", "", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "expect/jest-message-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "expect/jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "express-static-gzip/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "librechat-data-provider/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "svgo/css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.22.20", "", { "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.15", "@babel/types": "^7.22.19" } }, "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.22.20", "", { "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.15", "@babel/types": "^7.22.19" } }, "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], - - "workbox-build/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "workbox-build/source-map/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -9149,25 +8517,43 @@ "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@google/genai/google-auth-library/gaxios/rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@google/genai/google-auth-library/gaxios/rimraf/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "@google/genai/google-auth-library/gaxios/rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@aws-sdk/util-format-url/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], @@ -9175,6 +8561,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9185,37 +8579,51 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], @@ -9223,6 +8631,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9239,12 +8655,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9259,14 +8679,20 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9283,12 +8709,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9303,14 +8733,20 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9361,6 +8797,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9377,18 +8815,26 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9405,12 +8851,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9427,137 +8877,21 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + "@google/genai/google-auth-library/gaxios/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@google/genai/google-auth-library/gaxios/rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], @@ -9565,6 +8899,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9575,12 +8911,12 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9593,14 +8929,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9611,28 +8947,38 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9643,28 +8989,38 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9675,6 +9031,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9685,12 +9043,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9705,14 +9067,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9723,16 +9085,26 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9827,10 +9199,18 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9857,8 +9237,48 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], } } diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 44acf8ea26..1c698d08a3 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -1,4 +1,4 @@ -/** v0.8.3-rc2 */ +/** v0.8.3 */ module.exports = { roots: ['/src'], testEnvironment: 'jsdom', diff --git a/client/package.json b/client/package.json index 760d539695..250afc9990 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/frontend", - "version": "v0.8.3-rc2", + "version": "v0.8.3", "description": "", "type": "module", "scripts": { diff --git a/e2e/jestSetup.js b/e2e/jestSetup.js index 5372f02410..64c1a8546f 100644 --- a/e2e/jestSetup.js +++ b/e2e/jestSetup.js @@ -1,3 +1,3 @@ -// v0.8.3-rc2 +// v0.8.3 // See .env.test.example for an example of the '.env.test' file. require('dotenv').config({ path: './e2e/.env.test' }); diff --git a/helm/librechat/Chart.yaml b/helm/librechat/Chart.yaml index 8e14ae58ee..a2dff261c7 100755 --- a/helm/librechat/Chart.yaml +++ b/helm/librechat/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.9.9 +version: 2.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,7 +23,7 @@ version: 1.9.9 # It is recommended to use it with quotes. # renovate: image=registry.librechat.ai/danny-avila/librechat -appVersion: "v0.8.3-rc2" +appVersion: "v0.8.3" home: https://www.librechat.ai diff --git a/librechat.example.yaml b/librechat.example.yaml index 12f472bbae..03bb5f5bc2 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -2,7 +2,7 @@ # https://www.librechat.ai/docs/configuration/librechat_yaml # Configuration version (required) -version: 1.3.5 +version: 1.3.6 # Cache settings: Set to true to enable caching cache: true diff --git a/package-lock.json b/package-lock.json index 0672e76b3d..09c5219afb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "LibreChat", - "version": "v0.8.3-rc2", + "version": "v0.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "LibreChat", - "version": "v0.8.3-rc2", + "version": "v0.8.3", "license": "ISC", "workspaces": [ "api", @@ -46,7 +46,7 @@ }, "api": { "name": "@librechat/backend", - "version": "v0.8.3-rc2", + "version": "v0.8.3", "license": "ISC", "dependencies": { "@anthropic-ai/vertex-sdk": "^0.14.3", @@ -377,7 +377,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "v0.8.3-rc2", + "version": "v0.8.3", "license": "ISC", "dependencies": { "@ariakit/react": "^0.4.15", @@ -44151,7 +44151,7 @@ }, "packages/api": { "name": "@librechat/api", - "version": "1.7.24", + "version": "1.7.25", "license": "ISC", "devDependencies": { "@babel/preset-env": "^7.21.5", @@ -44267,7 +44267,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.4.53", + "version": "0.4.54", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -46091,7 +46091,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.301", + "version": "0.8.302", "license": "ISC", "dependencies": { "axios": "^1.13.5", @@ -46149,7 +46149,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.37", + "version": "0.0.38", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", diff --git a/package.json b/package.json index fd2d5191e1..ecbede482e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LibreChat", - "version": "v0.8.3-rc2", + "version": "v0.8.3", "description": "", "packageManager": "npm@11.10.0", "workspaces": [ diff --git a/packages/api/package.json b/packages/api/package.json index 3776e0e1d5..e4ca4ef3c5 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/api", - "version": "1.7.24", + "version": "1.7.25", "type": "commonjs", "description": "MCP services for LibreChat", "main": "dist/index.js", diff --git a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts index 78b854b4b0..374fe1d188 100644 --- a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts +++ b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts @@ -87,6 +87,8 @@ describe('getOpenAIConfig - Backward Compatibility', () => { defaultHeaders: { 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', 'x-librechat-thread-id': '{{LIBRECHAT_BODY_CONVERSATIONID}}', 'x-test-key': '{{TESTING_USER_VAR}}', }, diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index 58b471aa66..cdf9d6f14c 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -197,6 +197,8 @@ describe('getOpenAIConfig', () => { expect(result.configOptions?.defaultHeaders).toMatchObject({ 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', }); expect(result.llmConfig.include_reasoning).toBe(true); expect(result.provider).toBe('openrouter'); @@ -893,6 +895,8 @@ describe('getOpenAIConfig', () => { expect(result.configOptions?.defaultHeaders).toEqual({ 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', 'X-Custom-Header': 'custom-value', Authorization: 'Bearer custom-token', }); diff --git a/packages/api/src/endpoints/openai/config.ts b/packages/api/src/endpoints/openai/config.ts index 2540bbb815..5e8d8236ff 100644 --- a/packages/api/src/endpoints/openai/config.ts +++ b/packages/api/src/endpoints/openai/config.ts @@ -127,6 +127,8 @@ export function getOpenAIConfig( { 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', }, headers, ); diff --git a/packages/client/package.json b/packages/client/package.json index 46a03dc4af..13d1a4a8cc 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.4.53", + "version": "0.4.54", "description": "React components for LibreChat", "repository": { "type": "git", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index 5e67eaa744..a707aef448 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.8.301", + "version": "0.8.302", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 61be4b2116..e13521c019 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1736,9 +1736,9 @@ export enum TTSProviders { /** Enum for app-wide constants */ export enum Constants { /** Key for the app's version. */ - VERSION = 'v0.8.3-rc2', + VERSION = 'v0.8.3', /** Key for the Custom Config's version (librechat.yaml). */ - CONFIG_VERSION = '1.3.5', + CONFIG_VERSION = '1.3.6', /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */ NO_PARENT = '00000000-0000-0000-0000-000000000000', /** Standard value to use whatever the submission prelim. `responseMessageId` is */ diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index 699cb5fb19..e91bfb8886 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/data-schemas", - "version": "0.0.37", + "version": "0.0.38", "description": "Mongoose schemas and models for LibreChat", "type": "module", "main": "dist/index.cjs", From ad5c51f62b321bd6f714e946ab08190616c09ecf Mon Sep 17 00:00:00 2001 From: matt burnett Date: Tue, 10 Mar 2026 11:21:36 -0700 Subject: [PATCH 014/111] =?UTF-8?q?=E2=9B=88=EF=B8=8F=20fix:=20MCP=20Recon?= =?UTF-8?q?nection=20Storm=20Prevention=20with=20Circuit=20Breaker,=20Back?= =?UTF-8?q?off,=20and=20Tool=20Stubs=20(#12162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: MCP reconnection stability - circuit breaker, throttling, and cooldown retry * Comment and logging cleanup * fix broken tests --- api/server/services/MCP.js | 66 ++++- packages/api/src/mcp/UserConnectionManager.ts | 3 + .../src/mcp/__tests__/MCPConnection.test.ts | 239 ++++++++++++++++++ .../MCPConnectionAgentLifecycle.test.ts | 4 + packages/api/src/mcp/connection.ts | 117 ++++++++- .../oauth/OAuthReconnectionManager.test.ts | 162 +++++++++++- .../src/mcp/oauth/OAuthReconnectionManager.ts | 44 +++- .../oauth/OAuthReconnectionTracker.test.ts | 95 +++++++ .../src/mcp/oauth/OAuthReconnectionTracker.ts | 44 +++- 9 files changed, 736 insertions(+), 38 deletions(-) diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index ad1f9f5cc3..4f8cdc8195 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -34,6 +34,39 @@ const { reinitMCPServer } = require('./Tools/mcp'); const { getAppConfig } = require('./Config'); const { getLogStores } = require('~/cache'); +const lastReconnectAttempts = new Map(); +const RECONNECT_THROTTLE_MS = 10_000; + +const missingToolCache = new Map(); +const MISSING_TOOL_TTL_MS = 10_000; + +const unavailableMsg = + "This tool's MCP server is temporarily unavailable. Please try again shortly."; + +/** + * @param {string} toolName + * @param {string} serverName + */ +function createUnavailableToolStub(toolName, serverName) { + const normalizedToolKey = `${toolName}${Constants.mcp_delimiter}${normalizeServerName(serverName)}`; + const _call = async () => unavailableMsg; + const toolInstance = tool(_call, { + schema: { + type: 'object', + properties: { + input: { type: 'string', description: 'Input for the tool' }, + }, + required: [], + }, + name: normalizedToolKey, + description: unavailableMsg, + responseFormat: AgentConstants.CONTENT_AND_ARTIFACT, + }); + toolInstance.mcp = true; + toolInstance.mcpRawServerName = serverName; + return toolInstance; +} + function isEmptyObjectSchema(jsonSchema) { return ( jsonSchema != null && @@ -211,6 +244,16 @@ async function reconnectServer({ logger.debug( `[MCP][reconnectServer] serverName: ${serverName}, user: ${user?.id}, hasUserMCPAuthMap: ${!!userMCPAuthMap}`, ); + + const throttleKey = `${user.id}:${serverName}`; + const now = Date.now(); + const lastAttempt = lastReconnectAttempts.get(throttleKey) ?? 0; + if (now - lastAttempt < RECONNECT_THROTTLE_MS) { + logger.debug(`[MCP][reconnectServer] Throttled reconnect for ${serverName}`); + return null; + } + lastReconnectAttempts.set(throttleKey, now); + const runId = Constants.USE_PRELIM_RESPONSE_MESSAGE_ID; const flowId = `${user.id}:${serverName}:${Date.now()}`; const flowManager = getFlowStateManager(getLogStores(CacheKeys.FLOWS)); @@ -267,7 +310,7 @@ async function reconnectServer({ userMCPAuthMap, forceNew: true, returnOnOAuth: false, - connectionTimeout: Time.TWO_MINUTES, + connectionTimeout: Time.THIRTY_SECONDS, }); } finally { // Clean up abort handler to prevent memory leaks @@ -332,7 +375,7 @@ async function createMCPTools({ }); if (!result || !result.tools) { logger.warn(`[MCP][${serverName}] Failed to reinitialize MCP server.`); - return; + return []; } const serverTools = []; @@ -402,6 +445,14 @@ async function createMCPTool({ /** @type {LCTool | undefined} */ let toolDefinition = availableTools?.[toolKey]?.function; if (!toolDefinition) { + const cachedAt = missingToolCache.get(toolKey); + if (cachedAt && Date.now() - cachedAt < MISSING_TOOL_TTL_MS) { + logger.debug( + `[MCP][${serverName}][${toolName}] Tool in negative cache, returning unavailable stub.`, + ); + return createUnavailableToolStub(toolName, serverName); + } + logger.warn( `[MCP][${serverName}][${toolName}] Requested tool not found in available tools, re-initializing MCP server.`, ); @@ -415,11 +466,17 @@ async function createMCPTool({ streamId, }); toolDefinition = result?.availableTools?.[toolKey]?.function; + + if (!toolDefinition) { + missingToolCache.set(toolKey, Date.now()); + } } if (!toolDefinition) { - logger.warn(`[MCP][${serverName}][${toolName}] Tool definition not found, cannot create tool.`); - return; + logger.warn( + `[MCP][${serverName}][${toolName}] Tool definition not found, returning unavailable stub.`, + ); + return createUnavailableToolStub(toolName, serverName); } return createToolInstance({ @@ -720,4 +777,5 @@ module.exports = { getMCPSetupData, checkOAuthFlowStatus, getServerConnectionStatus, + createUnavailableToolStub, }; diff --git a/packages/api/src/mcp/UserConnectionManager.ts b/packages/api/src/mcp/UserConnectionManager.ts index c0ecd18fe2..0828b1720a 100644 --- a/packages/api/src/mcp/UserConnectionManager.ts +++ b/packages/api/src/mcp/UserConnectionManager.ts @@ -65,6 +65,9 @@ export abstract class UserConnectionManager { const userServerMap = this.userConnections.get(userId); let connection = forceNew ? undefined : userServerMap?.get(serverName); + if (forceNew) { + MCPConnection.clearCooldown(serverName); + } const now = Date.now(); // Check if user is idle diff --git a/packages/api/src/mcp/__tests__/MCPConnection.test.ts b/packages/api/src/mcp/__tests__/MCPConnection.test.ts index 4cca1b3316..5cb5606d57 100644 --- a/packages/api/src/mcp/__tests__/MCPConnection.test.ts +++ b/packages/api/src/mcp/__tests__/MCPConnection.test.ts @@ -559,3 +559,242 @@ describe('extractSSEErrorMessage', () => { }); }); }); + +/** + * Tests for circuit breaker logic. + * + * Uses standalone implementations that mirror the static/private circuit breaker + * methods in MCPConnection. Same approach as the error detection tests above. + */ +describe('MCPConnection Circuit Breaker', () => { + /** 5 cycles within 60s triggers a 30s cooldown */ + const CB_MAX_CYCLES = 5; + const CB_CYCLE_WINDOW_MS = 60_000; + const CB_CYCLE_COOLDOWN_MS = 30_000; + + /** 3 failed rounds within 120s triggers exponential backoff (30s - 300s) */ + const CB_MAX_FAILED_ROUNDS = 3; + const CB_FAILED_WINDOW_MS = 120_000; + const CB_BASE_BACKOFF_MS = 30_000; + const CB_MAX_BACKOFF_MS = 300_000; + + interface CircuitBreakerState { + cycleCount: number; + cycleWindowStart: number; + cooldownUntil: number; + failedRounds: number; + failedWindowStart: number; + failedBackoffUntil: number; + } + + function createCB(): CircuitBreakerState { + return { + cycleCount: 0, + cycleWindowStart: Date.now(), + cooldownUntil: 0, + failedRounds: 0, + failedWindowStart: Date.now(), + failedBackoffUntil: 0, + }; + } + + function isCircuitOpen(cb: CircuitBreakerState): boolean { + const now = Date.now(); + return now < cb.cooldownUntil || now < cb.failedBackoffUntil; + } + + function recordCycle(cb: CircuitBreakerState): void { + const now = Date.now(); + if (now - cb.cycleWindowStart > CB_CYCLE_WINDOW_MS) { + cb.cycleCount = 0; + cb.cycleWindowStart = now; + } + cb.cycleCount++; + if (cb.cycleCount >= CB_MAX_CYCLES) { + cb.cooldownUntil = now + CB_CYCLE_COOLDOWN_MS; + cb.cycleCount = 0; + cb.cycleWindowStart = now; + } + } + + function recordFailedRound(cb: CircuitBreakerState): void { + const now = Date.now(); + if (now - cb.failedWindowStart > CB_FAILED_WINDOW_MS) { + cb.failedRounds = 0; + cb.failedWindowStart = now; + } + cb.failedRounds++; + if (cb.failedRounds >= CB_MAX_FAILED_ROUNDS) { + const backoff = Math.min( + CB_BASE_BACKOFF_MS * Math.pow(2, cb.failedRounds - CB_MAX_FAILED_ROUNDS), + CB_MAX_BACKOFF_MS, + ); + cb.failedBackoffUntil = now + backoff; + } + } + + function resetFailedRounds(cb: CircuitBreakerState): void { + cb.failedRounds = 0; + cb.failedWindowStart = Date.now(); + cb.failedBackoffUntil = 0; + } + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('cycle tracking', () => { + it('should not trigger cooldown for fewer than 5 cycles', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_CYCLES - 1; i++) { + recordCycle(cb); + } + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should trigger 30s cooldown after 5 cycles within 60s', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_CYCLES; i++) { + recordCycle(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + jest.advanceTimersByTime(29_000); + expect(isCircuitOpen(cb)).toBe(true); + + jest.advanceTimersByTime(1_000); + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should reset cycle count when window expires', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_CYCLES - 1; i++) { + recordCycle(cb); + } + + jest.advanceTimersByTime(CB_CYCLE_WINDOW_MS + 1); + + recordCycle(cb); + expect(isCircuitOpen(cb)).toBe(false); + }); + }); + + describe('failed round tracking', () => { + it('should not trigger backoff for fewer than 3 failures', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_FAILED_ROUNDS - 1; i++) { + recordFailedRound(cb); + } + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should trigger 30s backoff after 3 failures within 120s', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_FAILED_ROUNDS; i++) { + recordFailedRound(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + jest.advanceTimersByTime(CB_BASE_BACKOFF_MS); + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should use exponential backoff based on failure count', () => { + jest.setSystemTime(Date.now()); + + const cb = createCB(); + + for (let i = 0; i < 3; i++) { + recordFailedRound(cb); + } + expect(cb.failedBackoffUntil - Date.now()).toBe(30_000); + + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(60_000); + + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(120_000); + + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(240_000); + + // capped at 300s + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(300_000); + }); + + it('should reset failed window when window expires', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + recordFailedRound(cb); + recordFailedRound(cb); + + jest.advanceTimersByTime(CB_FAILED_WINDOW_MS + 1); + + recordFailedRound(cb); + expect(isCircuitOpen(cb)).toBe(false); + }); + }); + + describe('resetFailedRounds', () => { + it('should clear failed round state on successful connection', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_FAILED_ROUNDS; i++) { + recordFailedRound(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + resetFailedRounds(cb); + expect(isCircuitOpen(cb)).toBe(false); + expect(cb.failedRounds).toBe(0); + expect(cb.failedBackoffUntil).toBe(0); + }); + }); + + describe('clearCooldown (registry deletion)', () => { + it('should allow connections after clearing circuit breaker state', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const registry = new Map(); + const serverName = 'test-server'; + + const cb = createCB(); + registry.set(serverName, cb); + + for (let i = 0; i < CB_MAX_CYCLES; i++) { + recordCycle(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + registry.delete(serverName); + + const newCb = createCB(); + expect(isCircuitOpen(newCb)).toBe(false); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts index 14e0694558..281bd590db 100644 --- a/packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts +++ b/packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts @@ -207,6 +207,7 @@ describe('MCPConnection Agent lifecycle – streamable-http', () => { }); afterEach(async () => { + MCPConnection.clearCooldown('test'); await safeDisconnect(conn); conn = null; jest.restoreAllMocks(); @@ -366,6 +367,7 @@ describe('MCPConnection Agent lifecycle – SSE', () => { }); afterEach(async () => { + MCPConnection.clearCooldown('test-sse'); await safeDisconnect(conn); conn = null; jest.restoreAllMocks(); @@ -453,6 +455,7 @@ describe('Regression: old per-request Agent pattern leaks agents', () => { }); afterEach(async () => { + MCPConnection.clearCooldown('test-regression'); await safeDisconnect(conn); conn = null; jest.restoreAllMocks(); @@ -675,6 +678,7 @@ describe('MCPConnection SSE GET stream recovery – integration', () => { }); afterEach(async () => { + MCPConnection.clearCooldown('test-sse-recovery'); await safeDisconnect(conn); conn = null; jest.restoreAllMocks(); diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index 83f1af1824..cac0a4afc5 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -71,6 +71,25 @@ const FIVE_MINUTES = 5 * 60 * 1000; const DEFAULT_TIMEOUT = 60000; /** SSE connections through proxies may need longer initial handshake time */ const SSE_CONNECT_TIMEOUT = 120000; +const DEFAULT_INIT_TIMEOUT = 30000; + +interface CircuitBreakerState { + cycleCount: number; + cycleWindowStart: number; + cooldownUntil: number; + failedRounds: number; + failedWindowStart: number; + failedBackoffUntil: number; +} + +const CB_MAX_CYCLES = 5; +const CB_CYCLE_WINDOW_MS = 60_000; +const CB_CYCLE_COOLDOWN_MS = 30_000; + +const CB_MAX_FAILED_ROUNDS = 3; +const CB_FAILED_WINDOW_MS = 120_000; +const CB_BASE_BACKOFF_MS = 30_000; +const CB_MAX_BACKOFF_MS = 300_000; /** Default body timeout for Streamable HTTP GET SSE streams that idle between server pushes */ const DEFAULT_SSE_READ_TIMEOUT = FIVE_MINUTES; @@ -274,6 +293,80 @@ export class MCPConnection extends EventEmitter { */ public readonly createdAt: number; + private static circuitBreakers: Map = new Map(); + + public static clearCooldown(serverName: string): void { + MCPConnection.circuitBreakers.delete(serverName); + logger.debug(`[MCP][${serverName}] Circuit breaker state cleared`); + } + + private getCircuitBreaker(): CircuitBreakerState { + let cb = MCPConnection.circuitBreakers.get(this.serverName); + if (!cb) { + cb = { + cycleCount: 0, + cycleWindowStart: Date.now(), + cooldownUntil: 0, + failedRounds: 0, + failedWindowStart: Date.now(), + failedBackoffUntil: 0, + }; + MCPConnection.circuitBreakers.set(this.serverName, cb); + } + return cb; + } + + private isCircuitOpen(): boolean { + const cb = this.getCircuitBreaker(); + const now = Date.now(); + return now < cb.cooldownUntil || now < cb.failedBackoffUntil; + } + + private recordCycle(): void { + const cb = this.getCircuitBreaker(); + const now = Date.now(); + if (now - cb.cycleWindowStart > CB_CYCLE_WINDOW_MS) { + cb.cycleCount = 0; + cb.cycleWindowStart = now; + } + cb.cycleCount++; + if (cb.cycleCount >= CB_MAX_CYCLES) { + cb.cooldownUntil = now + CB_CYCLE_COOLDOWN_MS; + cb.cycleCount = 0; + cb.cycleWindowStart = now; + logger.warn( + `${this.getLogPrefix()} Circuit breaker: too many cycles, cooling down for ${CB_CYCLE_COOLDOWN_MS}ms`, + ); + } + } + + private recordFailedRound(): void { + const cb = this.getCircuitBreaker(); + const now = Date.now(); + if (now - cb.failedWindowStart > CB_FAILED_WINDOW_MS) { + cb.failedRounds = 0; + cb.failedWindowStart = now; + } + cb.failedRounds++; + if (cb.failedRounds >= CB_MAX_FAILED_ROUNDS) { + const backoff = Math.min( + CB_BASE_BACKOFF_MS * Math.pow(2, cb.failedRounds - CB_MAX_FAILED_ROUNDS), + CB_MAX_BACKOFF_MS, + ); + cb.failedBackoffUntil = now + backoff; + logger.warn( + `${this.getLogPrefix()} Circuit breaker: too many failures, backing off for ${backoff}ms`, + ); + } + } + + private resetFailedRounds(): void { + const cb = this.getCircuitBreaker(); + cb.failedRounds = 0; + cb.failedWindowStart = Date.now(); + cb.failedBackoffUntil = 0; + } + setRequestHeaders(headers: Record | null): void { if (!headers) { return; @@ -686,6 +779,12 @@ export class MCPConnection extends EventEmitter { return; } + if (this.isCircuitOpen()) { + this.connectionState = 'error'; + this.emit('connectionChange', 'error'); + throw new Error(`${this.getLogPrefix()} Circuit breaker is open, connection attempt blocked`); + } + this.emit('connectionChange', 'connecting'); this.connectPromise = (async () => { @@ -703,7 +802,7 @@ export class MCPConnection extends EventEmitter { this.transport = await runOutsideTracing(() => this.constructTransport(this.options)); this.patchTransportSend(); - const connectTimeout = this.options.initTimeout ?? 120000; + const connectTimeout = this.options.initTimeout ?? DEFAULT_INIT_TIMEOUT; await runOutsideTracing(() => withTimeout( this.client.connect(this.transport!), @@ -716,6 +815,7 @@ export class MCPConnection extends EventEmitter { this.connectionState = 'connected'; this.emit('connectionChange', 'connected'); this.reconnectAttempts = 0; + this.resetFailedRounds(); } catch (error) { // Check if it's a rate limit error - stop immediately to avoid making it worse if (this.isRateLimitError(error)) { @@ -817,6 +917,7 @@ export class MCPConnection extends EventEmitter { this.connectionState = 'error'; this.emit('connectionChange', 'error'); + this.recordFailedRound(); throw error; } finally { this.connectPromise = null; @@ -866,7 +967,8 @@ export class MCPConnection extends EventEmitter { async connect(): Promise { try { - await this.disconnect(); + // preserve cycle tracking across reconnects so the circuit breaker can detect rapid cycling + await this.disconnect(false); await this.connectClient(); if (!(await this.isConnected())) { throw new Error('Connection not established'); @@ -906,7 +1008,7 @@ export class MCPConnection extends EventEmitter { isTransient, } = extractSSEErrorMessage(error); - if (errorCode === 404) { + if (errorCode === 400 || errorCode === 404 || errorCode === 405) { const hasSession = 'sessionId' in transport && (transport as { sessionId?: string }).sessionId != null && @@ -914,14 +1016,14 @@ export class MCPConnection extends EventEmitter { if (!hasSession && errorMessage.toLowerCase().includes('failed to open sse stream')) { logger.warn( - `${this.getLogPrefix()} SSE stream not available (404), no session. Ignoring.`, + `${this.getLogPrefix()} SSE stream not available (${errorCode}), no session. Ignoring.`, ); return; } if (hasSession) { logger.warn( - `${this.getLogPrefix()} 404 with active session — session lost, triggering reconnection.`, + `${this.getLogPrefix()} ${errorCode} with active session — session lost, triggering reconnection.`, ); } } @@ -992,7 +1094,7 @@ export class MCPConnection extends EventEmitter { await Promise.all(closing); } - public async disconnect(): Promise { + public async disconnect(resetCycleTracking = true): Promise { try { if (this.transport) { await this.client.close(); @@ -1006,6 +1108,9 @@ export class MCPConnection extends EventEmitter { this.emit('connectionChange', 'disconnected'); } finally { this.connectPromise = null; + if (!resetCycleTracking) { + this.recordCycle(); + } } } diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts b/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts index d3447eaeb8..d889da4f2f 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts @@ -253,17 +253,21 @@ describe('OAuthReconnectionManager', () => { expect(mockMCPManager.disconnectUserConnection).toHaveBeenCalledWith(userId, 'server1'); }); - it('should not reconnect servers with expired tokens', async () => { + it('should not reconnect servers with expired tokens and no refresh token', async () => { const userId = 'user-123'; const oauthServers = new Set(['server1']); (mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers); - // server1: has expired token - tokenMethods.findToken.mockResolvedValue({ - userId, - identifier: 'mcp:server1', - expiresAt: new Date(Date.now() - 3600000), // 1 hour ago - } as unknown as MCPOAuthTokens); + tokenMethods.findToken.mockImplementation(async ({ identifier }) => { + if (identifier === 'mcp:server1') { + return { + userId, + identifier, + expiresAt: new Date(Date.now() - 3600000), + } as unknown as MCPOAuthTokens; + } + return null; + }); await reconnectionManager.reconnectServers(userId); @@ -272,6 +276,87 @@ describe('OAuthReconnectionManager', () => { expect(mockMCPManager.getUserConnection).not.toHaveBeenCalled(); }); + it('should reconnect servers with expired access token but valid refresh token', async () => { + const userId = 'user-123'; + const oauthServers = new Set(['server1']); + (mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers); + + tokenMethods.findToken.mockImplementation(async ({ identifier }) => { + if (identifier === 'mcp:server1') { + return { + userId, + identifier, + expiresAt: new Date(Date.now() - 3600000), + } as unknown as MCPOAuthTokens; + } + if (identifier === 'mcp:server1:refresh') { + return { + userId, + identifier, + } as unknown as MCPOAuthTokens; + } + return null; + }); + + const mockNewConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn(), + }; + mockMCPManager.getUserConnection.mockResolvedValue( + mockNewConnection as unknown as MCPConnection, + ); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + await reconnectionManager.reconnectServers(userId); + + expect(reconnectionTracker.isActive(userId, 'server1')).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockMCPManager.getUserConnection).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'server1' }), + ); + }); + + it('should reconnect when access token is TTL-deleted but refresh token exists', async () => { + const userId = 'user-123'; + const oauthServers = new Set(['server1']); + (mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers); + + tokenMethods.findToken.mockImplementation(async ({ identifier }) => { + if (identifier === 'mcp:server1:refresh') { + return { + userId, + identifier, + } as unknown as MCPOAuthTokens; + } + return null; + }); + + const mockNewConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn(), + }; + mockMCPManager.getUserConnection.mockResolvedValue( + mockNewConnection as unknown as MCPConnection, + ); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + await reconnectionManager.reconnectServers(userId); + + expect(reconnectionTracker.isActive(userId, 'server1')).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockMCPManager.getUserConnection).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'server1' }), + ); + }); + it('should handle connection that returns but is not connected', async () => { const userId = 'user-123'; const oauthServers = new Set(['server1']); @@ -336,6 +421,69 @@ describe('OAuthReconnectionManager', () => { }); }); + describe('reconnectServer', () => { + let reconnectionTracker: OAuthReconnectionTracker; + beforeEach(async () => { + reconnectionTracker = new OAuthReconnectionTracker(); + reconnectionManager = await OAuthReconnectionManager.createInstance( + flowManager, + tokenMethods, + reconnectionTracker, + ); + }); + + it('should return true on successful reconnection', async () => { + const userId = 'user-123'; + const serverName = 'server1'; + + const mockConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn(), + }; + mockMCPManager.getUserConnection.mockResolvedValue( + mockConnection as unknown as MCPConnection, + ); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + const result = await reconnectionManager.reconnectServer(userId, serverName); + expect(result).toBe(true); + }); + + it('should return false on failed reconnection', async () => { + const userId = 'user-123'; + const serverName = 'server1'; + + mockMCPManager.getUserConnection.mockRejectedValue(new Error('Connection failed')); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + const result = await reconnectionManager.reconnectServer(userId, serverName); + expect(result).toBe(false); + }); + + it('should return false when MCPManager is not available', async () => { + const userId = 'user-123'; + const serverName = 'server1'; + + (OAuthReconnectionManager as unknown as { instance: null }).instance = null; + (MCPManager.getInstance as jest.Mock).mockImplementation(() => { + throw new Error('MCPManager has not been initialized.'); + }); + + const managerWithoutMCP = await OAuthReconnectionManager.createInstance( + flowManager, + tokenMethods, + reconnectionTracker, + ); + + const result = await managerWithoutMCP.reconnectServer(userId, serverName); + expect(result).toBe(false); + }); + }); + describe('reconnection staggering', () => { let reconnectionTracker: OAuthReconnectionTracker; diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts b/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts index f14c4abf15..7afe992772 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts @@ -96,6 +96,24 @@ export class OAuthReconnectionManager { } } + /** + * Attempts to reconnect a single OAuth MCP server. + * @returns true if reconnection succeeded, false otherwise. + */ + public async reconnectServer(userId: string, serverName: string): Promise { + if (this.mcpManager == null) { + return false; + } + + this.reconnectionsTracker.setActive(userId, serverName); + try { + await this.tryReconnect(userId, serverName); + return !this.reconnectionsTracker.isFailed(userId, serverName); + } catch { + return false; + } + } + public clearReconnection(userId: string, serverName: string) { this.reconnectionsTracker.removeFailed(userId, serverName); this.reconnectionsTracker.removeActive(userId, serverName); @@ -174,23 +192,31 @@ export class OAuthReconnectionManager { } } - // if the server has no tokens for the user, don't attempt to reconnect + // if the server has a valid (non-expired) access token, allow reconnect const accessToken = await this.tokenMethods.findToken({ userId, type: 'mcp_oauth', identifier: `mcp:${serverName}`, }); - if (accessToken == null) { + + if (accessToken != null) { + const now = new Date(); + if (!accessToken.expiresAt || accessToken.expiresAt >= now) { + return true; + } + } + + // if the access token is expired or TTL-deleted, fall back to refresh token + const refreshToken = await this.tokenMethods.findToken({ + userId, + type: 'mcp_oauth', + identifier: `mcp:${serverName}:refresh`, + }); + + if (refreshToken == null) { return false; } - // if the token has expired, don't attempt to reconnect - const now = new Date(); - if (accessToken.expiresAt && accessToken.expiresAt < now) { - return false; - } - - // …otherwise, we're good to go with the reconnect attempt return true; } } diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts index 68ac1d027e..206fe96ef1 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts @@ -397,6 +397,101 @@ describe('OAuthReconnectTracker', () => { }); }); + describe('cooldown-based retry', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should return true from isFailed within first cooldown period (5 min)', () => { + const now = Date.now(); + jest.setSystemTime(now); + + tracker.setFailed(userId, serverName); + expect(tracker.isFailed(userId, serverName)).toBe(true); + + jest.advanceTimersByTime(4 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + }); + + it('should return false from isFailed after first cooldown elapses (5 min)', () => { + const now = Date.now(); + jest.setSystemTime(now); + + tracker.setFailed(userId, serverName); + expect(tracker.isFailed(userId, serverName)).toBe(true); + + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + + it('should use progressive cooldown schedule (5m, 10m, 20m, 30m)', () => { + const now = Date.now(); + jest.setSystemTime(now); + + // First failure: 5 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + // Second failure: 10 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(9 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + // Third failure: 20 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(19 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + // Fourth failure: 30 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(29 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + + it('should cap cooldown at 30 min for attempts beyond 4', () => { + const now = Date.now(); + jest.setSystemTime(now); + + for (let i = 0; i < 5; i++) { + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(30 * 60 * 1000); + } + + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(29 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + + it('should fully reset metadata on removeFailed', () => { + const now = Date.now(); + jest.setSystemTime(now); + + tracker.setFailed(userId, serverName); + tracker.setFailed(userId, serverName); + tracker.setFailed(userId, serverName); + + tracker.removeFailed(userId, serverName); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + }); + describe('timestamp tracking edge cases', () => { beforeEach(() => { jest.useFakeTimers(); diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts index 9f6ef4abd3..504ea7d43a 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts @@ -1,6 +1,12 @@ +interface FailedMeta { + attempts: number; + lastFailedAt: number; +} + +const COOLDOWN_SCHEDULE_MS = [5 * 60 * 1000, 10 * 60 * 1000, 20 * 60 * 1000, 30 * 60 * 1000]; + export class OAuthReconnectionTracker { - /** Map of userId -> Set of serverNames that have failed reconnection */ - private failed: Map> = new Map(); + private failedMeta: Map> = new Map(); /** Map of userId -> Set of serverNames that are actively reconnecting */ private active: Map> = new Map(); /** Map of userId:serverName -> timestamp when reconnection started */ @@ -9,7 +15,17 @@ export class OAuthReconnectionTracker { private readonly RECONNECTION_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes public isFailed(userId: string, serverName: string): boolean { - return this.failed.get(userId)?.has(serverName) ?? false; + const meta = this.failedMeta.get(userId)?.get(serverName); + if (!meta) { + return false; + } + const idx = Math.min(meta.attempts - 1, COOLDOWN_SCHEDULE_MS.length - 1); + const cooldown = COOLDOWN_SCHEDULE_MS[idx]; + const elapsed = Date.now() - meta.lastFailedAt; + if (elapsed >= cooldown) { + return false; + } + return true; } /** Check if server is in the active set (original simple check) */ @@ -48,11 +64,15 @@ export class OAuthReconnectionTracker { } public setFailed(userId: string, serverName: string): void { - if (!this.failed.has(userId)) { - this.failed.set(userId, new Set()); + if (!this.failedMeta.has(userId)) { + this.failedMeta.set(userId, new Map()); } - - this.failed.get(userId)?.add(serverName); + const userMap = this.failedMeta.get(userId)!; + const existing = userMap.get(serverName); + userMap.set(serverName, { + attempts: (existing?.attempts ?? 0) + 1, + lastFailedAt: Date.now(), + }); } public setActive(userId: string, serverName: string): void { @@ -68,10 +88,10 @@ export class OAuthReconnectionTracker { } public removeFailed(userId: string, serverName: string): void { - const userServers = this.failed.get(userId); - userServers?.delete(serverName); - if (userServers?.size === 0) { - this.failed.delete(userId); + const userMap = this.failedMeta.get(userId); + userMap?.delete(serverName); + if (userMap?.size === 0) { + this.failedMeta.delete(userId); } } @@ -94,7 +114,7 @@ export class OAuthReconnectionTracker { activeTimestamps: number; } { return { - usersWithFailedServers: this.failed.size, + usersWithFailedServers: this.failedMeta.size, usersWithActiveReconnections: this.active.size, activeTimestamps: this.activeTimestamps.size, }; From eb6328c1d980274b4db6d6af1b5184ec67d56636 Mon Sep 17 00:00:00 2001 From: Oreon Lothamer <73498677+oreonl@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:04:35 -1000 Subject: [PATCH 015/111] =?UTF-8?q?=F0=9F=9B=A4=EF=B8=8F=20fix:=20Base=20U?= =?UTF-8?q?RL=20Fallback=20for=20Path-based=20OAuth=20Discovery=20in=20Tok?= =?UTF-8?q?en=20Refresh=20(#12164)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add base URL fallback for path-based OAuth discovery in token refresh The two `refreshOAuthTokens` paths in `MCPOAuthHandler` were missing the origin-URL fallback that `initiateOAuthFlow` already had. With MCP SDK 1.27.1, `buildDiscoveryUrls` appends the server path to the `.well-known` URL (e.g. `/.well-known/oauth-authorization-server/mcp`), which returns 404 for servers like Sentry that only expose the root discovery endpoint (`/.well-known/oauth-authorization-server`). Without the fallback, discovery returns null during refresh, the token endpoint resolves to the wrong URL, and users are prompted to re-authenticate every time their access token expires instead of the refresh token being exchanged silently. Both refresh paths now mirror the `initiateOAuthFlow` pattern: if discovery fails and the server URL has a non-root path, retry with just the origin URL. Co-Authored-By: Claude Sonnet 4.6 * refactor: extract discoverWithOriginFallback helper; add tests Extract the duplicated path-based URL retry logic from both `refreshOAuthTokens` branches into a single private static helper `discoverWithOriginFallback`, reducing the risk of the two paths drifting in the future. Add three tests covering the new behaviour: - stored clientInfo path: asserts discovery is called twice (path then origin) and that the token endpoint from the origin discovery is used - auto-discovered path: same assertions for the branchless path - root URL: asserts discovery is called only once when the server URL already has no path component Co-Authored-By: Claude Sonnet 4.6 * refactor: use discoverWithOriginFallback in discoverMetadata too Remove the inline duplicate of the origin-fallback logic from `discoverMetadata` and replace it with a call to the shared `discoverWithOriginFallback` helper, giving all three discovery sites a single implementation. Co-Authored-By: Claude Sonnet 4.6 * test: use mock.calls + .href/.toString() for URL assertions Replace brittle `toHaveBeenNthCalledWith(new URL(...))` comparisons with `expect.any(URL)` matchers and explicit `.href`/`.toString()` checks on the captured call args, consistent with the existing mock.calls pattern used throughout handler.test.ts. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../api/src/mcp/__tests__/handler.test.ts | 162 ++++++++++++++++++ packages/api/src/mcp/oauth/handler.ts | 49 +++--- 2 files changed, 191 insertions(+), 20 deletions(-) diff --git a/packages/api/src/mcp/__tests__/handler.test.ts b/packages/api/src/mcp/__tests__/handler.test.ts index db88afe581..e5d94b23e3 100644 --- a/packages/api/src/mcp/__tests__/handler.test.ts +++ b/packages/api/src/mcp/__tests__/handler.test.ts @@ -1439,5 +1439,167 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { }), ); }); + + describe('path-based URL origin fallback', () => { + it('retries with origin URL when path-based discovery fails (stored clientInfo path)', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + grant_types: ['authorization_code', 'refresh_token'], + }, + }; + + const originMetadata = { + issuer: 'https://mcp.sentry.dev/', + authorization_endpoint: 'https://mcp.sentry.dev/oauth/authorize', + token_endpoint: 'https://mcp.sentry.dev/oauth/token', + token_endpoint_auth_methods_supported: ['client_secret_post'], + response_types_supported: ['code'], + jwks_uri: 'https://mcp.sentry.dev/.well-known/jwks.json', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + } as AuthorizationServerMetadata; + + // First call (path-based URL) fails, second call (origin URL) succeeds + mockDiscoverAuthorizationServerMetadata + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(originMetadata); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + // Discovery attempted twice: once with path URL, once with origin URL + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 1, + expect.any(URL), + expect.any(Object), + ); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 2, + expect.any(URL), + expect.any(Object), + ); + const firstDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[0][0] as URL; + const secondDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[1][0] as URL; + expect(firstDiscoveryUrl).toBeInstanceOf(URL); + expect(firstDiscoveryUrl.href).toBe('https://mcp.sentry.dev/mcp'); + expect(secondDiscoveryUrl).toBeInstanceOf(URL); + expect(secondDiscoveryUrl.href).toBe('https://mcp.sentry.dev/'); + + // Token endpoint from origin discovery metadata is used + expect(mockFetch).toHaveBeenCalled(); + const [fetchUrl, fetchOptions] = mockFetch.mock.calls[0]; + expect(String(fetchUrl)).toBe('https://mcp.sentry.dev/oauth/token'); + expect(fetchOptions).toEqual(expect.objectContaining({ method: 'POST' })); + expect(result.access_token).toBe('new-access-token'); + }); + + it('retries with origin URL when path-based discovery fails (auto-discovered path)', async () => { + // No clientInfo — uses the auto-discovered branch + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + }; + + const originMetadata = { + issuer: 'https://mcp.sentry.dev/', + authorization_endpoint: 'https://mcp.sentry.dev/oauth/authorize', + token_endpoint: 'https://mcp.sentry.dev/oauth/token', + response_types_supported: ['code'], + jwks_uri: 'https://mcp.sentry.dev/.well-known/jwks.json', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + } as AuthorizationServerMetadata; + + // First call (path-based URL) fails, second call (origin URL) succeeds + mockDiscoverAuthorizationServerMetadata + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(originMetadata); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + // Discovery attempted twice: once with path URL, once with origin URL + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 1, + expect.any(URL), + expect.any(Object), + ); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 2, + expect.any(URL), + expect.any(Object), + ); + const firstDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[0][0] as URL; + const secondDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[1][0] as URL; + expect(firstDiscoveryUrl).toBeInstanceOf(URL); + expect(firstDiscoveryUrl.href).toBe('https://mcp.sentry.dev/mcp'); + expect(secondDiscoveryUrl).toBeInstanceOf(URL); + expect(secondDiscoveryUrl.href).toBe('https://mcp.sentry.dev/'); + + // Token endpoint from origin discovery metadata is used + expect(mockFetch).toHaveBeenCalled(); + const [fetchUrl, fetchOptions] = mockFetch.mock.calls[0]; + expect(fetchUrl).toBeInstanceOf(URL); + expect(fetchUrl.toString()).toBe('https://mcp.sentry.dev/oauth/token'); + expect(fetchOptions).toEqual(expect.objectContaining({ method: 'POST' })); + expect(result.access_token).toBe('new-access-token'); + }); + + it('does not retry with origin when server URL has no path (root URL)', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://auth.example.com/', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + }; + + // Root URL discovery fails — no retry + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'new-token', expires_in: 3600 }), + } as Response); + + await MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}); + + // Only one discovery attempt for a root URL + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 92b9f1211c..6ef444bf47 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -161,20 +161,7 @@ export class MCPOAuthHandler { logger.debug( `[MCPOAuth] Discovering OAuth metadata from ${sanitizeUrlForLogging(authServerUrl)}`, ); - let rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl, { - fetchFn, - }); - - // If discovery failed and we're using a path-based URL, try the base URL - if (!rawMetadata && authServerUrl.pathname !== '/') { - const baseUrl = new URL(authServerUrl.origin); - logger.debug( - `[MCPOAuth] Discovery failed with path, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`, - ); - rawMetadata = await discoverAuthorizationServerMetadata(baseUrl, { - fetchFn, - }); - } + const rawMetadata = await this.discoverWithOriginFallback(authServerUrl, fetchFn); if (!rawMetadata) { /** @@ -221,6 +208,27 @@ export class MCPOAuthHandler { }; } + /** + * Discovers OAuth authorization server metadata with origin-URL fallback. + * If discovery fails for a path-based URL, retries with just the origin. + * Mirrors the fallback behavior in `discoverMetadata` and `initiateOAuthFlow`. + */ + private static async discoverWithOriginFallback( + serverUrl: URL, + fetchFn: FetchLike, + ): ReturnType { + const metadata = await discoverAuthorizationServerMetadata(serverUrl, { fetchFn }); + // If discovery failed and we're using a path-based URL, try the base URL + if (!metadata && serverUrl.pathname !== '/') { + const baseUrl = new URL(serverUrl.origin); + logger.debug( + `[MCPOAuth] Discovery failed with path, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`, + ); + return discoverAuthorizationServerMetadata(baseUrl, { fetchFn }); + } + return metadata; + } + /** * Registers an OAuth client dynamically */ @@ -735,9 +743,10 @@ export class MCPOAuthHandler { throw new Error('No token URL available for refresh'); } else { /** Auto-discover OAuth configuration for refresh */ - const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl, { - fetchFn: this.createOAuthFetch(oauthHeaders), - }); + const serverUrl = new URL(metadata.serverUrl); + const fetchFn = this.createOAuthFetch(oauthHeaders); + const oauthMetadata = await this.discoverWithOriginFallback(serverUrl, fetchFn); + if (!oauthMetadata) { /** * No metadata discovered - use fallback /token endpoint. @@ -911,9 +920,9 @@ export class MCPOAuthHandler { } /** Auto-discover OAuth configuration for refresh */ - const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl, { - fetchFn: this.createOAuthFetch(oauthHeaders), - }); + const serverUrl = new URL(metadata.serverUrl); + const fetchFn = this.createOAuthFetch(oauthHeaders); + const oauthMetadata = await this.discoverWithOriginFallback(serverUrl, fetchFn); let tokenUrl: URL; if (!oauthMetadata?.token_endpoint) { From c0e876a2e6f6346b76604587b5d4ef1e74ca9ad8 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Mar 2026 16:19:07 -0400 Subject: [PATCH 016/111] =?UTF-8?q?=F0=9F=94=84=20refactor:=20OAuth=20Meta?= =?UTF-8?q?data=20Discovery=20with=20Origin=20Fallback=20(#12170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔄 refactor: OAuth Metadata Discovery with Origin Fallback Updated the `discoverWithOriginFallback` method to improve the handling of OAuth authorization server metadata discovery. The method now retries with the origin URL when discovery fails for a path-based URL, ensuring consistent behavior across `discoverMetadata` and token refresh flows. This change reduces code duplication and enhances the reliability of the OAuth flow by providing a unified implementation for origin fallback logic. * 🧪 test: Add tests for OAuth Token Refresh with Origin Fallback Introduced new tests for the `refreshOAuthTokens` method in `MCPOAuthHandler` to validate the retry mechanism with the origin URL when path-based discovery fails. The tests cover scenarios where the first discovery attempt throws an error and the subsequent attempt succeeds, as well as cases where the discovery fails entirely. This enhances the reliability of the OAuth token refresh process by ensuring proper handling of discovery failures. * chore: imports order * fix: Improve Base URL Logging and Metadata Discovery in MCPOAuthHandler Updated the logging to use a consistent base URL object when handling discovery failures in the MCPOAuthHandler. This change enhances error reporting by ensuring that the base URL is logged correctly, and it refines the metadata discovery process by returning the result of the discovery attempt with the base URL, improving the reliability of the OAuth flow. --- .../api/src/mcp/__tests__/handler.test.ts | 141 +++++++++++++++++- packages/api/src/mcp/oauth/handler.ts | 22 ++- .../MCPReinitRecovery.integration.test.ts | 6 +- 3 files changed, 153 insertions(+), 16 deletions(-) diff --git a/packages/api/src/mcp/__tests__/handler.test.ts b/packages/api/src/mcp/__tests__/handler.test.ts index e5d94b23e3..3b68d88e9c 100644 --- a/packages/api/src/mcp/__tests__/handler.test.ts +++ b/packages/api/src/mcp/__tests__/handler.test.ts @@ -1498,20 +1498,19 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { ); const firstDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[0][0] as URL; const secondDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[1][0] as URL; - expect(firstDiscoveryUrl).toBeInstanceOf(URL); expect(firstDiscoveryUrl.href).toBe('https://mcp.sentry.dev/mcp'); - expect(secondDiscoveryUrl).toBeInstanceOf(URL); expect(secondDiscoveryUrl.href).toBe('https://mcp.sentry.dev/'); - // Token endpoint from origin discovery metadata is used + // Token endpoint from origin discovery metadata is used (string in stored-clientInfo branch) expect(mockFetch).toHaveBeenCalled(); const [fetchUrl, fetchOptions] = mockFetch.mock.calls[0]; - expect(String(fetchUrl)).toBe('https://mcp.sentry.dev/oauth/token'); + expect(typeof fetchUrl).toBe('string'); + expect(fetchUrl).toBe('https://mcp.sentry.dev/oauth/token'); expect(fetchOptions).toEqual(expect.objectContaining({ method: 'POST' })); expect(result.access_token).toBe('new-access-token'); }); - it('retries with origin URL when path-based discovery fails (auto-discovered path)', async () => { + it('retries with origin URL when path-based discovery fails (no stored clientInfo)', async () => { // No clientInfo — uses the auto-discovered branch const metadata = { serverName: 'sentry', @@ -1563,12 +1562,10 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { ); const firstDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[0][0] as URL; const secondDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[1][0] as URL; - expect(firstDiscoveryUrl).toBeInstanceOf(URL); expect(firstDiscoveryUrl.href).toBe('https://mcp.sentry.dev/mcp'); - expect(secondDiscoveryUrl).toBeInstanceOf(URL); expect(secondDiscoveryUrl.href).toBe('https://mcp.sentry.dev/'); - // Token endpoint from origin discovery metadata is used + // Token endpoint from origin discovery metadata is used (URL object in auto-discovered branch) expect(mockFetch).toHaveBeenCalled(); const [fetchUrl, fetchOptions] = mockFetch.mock.calls[0]; expect(fetchUrl).toBeInstanceOf(URL); @@ -1577,6 +1574,46 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { expect(result.access_token).toBe('new-access-token'); }); + it('falls back to /token when both path and origin discovery fail', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + grant_types: ['authorization_code', 'refresh_token'], + }, + }; + + // Both path AND origin discovery return undefined + mockDiscoverAuthorizationServerMetadata + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + + // Falls back to /token relative to server URL origin + const [fetchUrl] = mockFetch.mock.calls[0]; + expect(String(fetchUrl)).toBe('https://mcp.sentry.dev/token'); + expect(result.access_token).toBe('new-access-token'); + }); + it('does not retry with origin when server URL has no path (root URL)', async () => { const metadata = { serverName: 'test-server', @@ -1600,6 +1637,94 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { // Only one discovery attempt for a root URL expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(1); }); + + it('retries with origin when path-based discovery throws', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + grant_types: ['authorization_code', 'refresh_token'], + }, + }; + + const originMetadata = { + issuer: 'https://mcp.sentry.dev/', + authorization_endpoint: 'https://mcp.sentry.dev/oauth/authorize', + token_endpoint: 'https://mcp.sentry.dev/oauth/token', + token_endpoint_auth_methods_supported: ['client_secret_post'], + response_types_supported: ['code'], + } as AuthorizationServerMetadata; + + // First call throws, second call succeeds + mockDiscoverAuthorizationServerMetadata + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce(originMetadata); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + const [fetchUrl] = mockFetch.mock.calls[0]; + expect(String(fetchUrl)).toBe('https://mcp.sentry.dev/oauth/token'); + expect(result.access_token).toBe('new-access-token'); + }); + + it('propagates the throw when root URL discovery throws', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://auth.example.com/', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + }; + + mockDiscoverAuthorizationServerMetadata.mockRejectedValueOnce( + new Error('Discovery failed'), + ); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('Discovery failed'); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(1); + }); + + it('propagates the throw when both path and origin discovery throw', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + }; + + mockDiscoverAuthorizationServerMetadata + .mockRejectedValueOnce(new Error('Network error')) + .mockRejectedValueOnce(new Error('Origin also failed')); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('Origin also failed'); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + }); }); }); }); diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 6ef444bf47..83e855591e 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -209,16 +209,28 @@ export class MCPOAuthHandler { } /** - * Discovers OAuth authorization server metadata with origin-URL fallback. - * If discovery fails for a path-based URL, retries with just the origin. - * Mirrors the fallback behavior in `discoverMetadata` and `initiateOAuthFlow`. + * Discovers OAuth authorization server metadata, retrying with just the origin + * when discovery fails for a path-based URL. Shared implementation used by + * `discoverMetadata` and both `refreshOAuthTokens` branches. */ private static async discoverWithOriginFallback( serverUrl: URL, fetchFn: FetchLike, ): ReturnType { - const metadata = await discoverAuthorizationServerMetadata(serverUrl, { fetchFn }); - // If discovery failed and we're using a path-based URL, try the base URL + let metadata: Awaited>; + try { + metadata = await discoverAuthorizationServerMetadata(serverUrl, { fetchFn }); + } catch (err) { + if (serverUrl.pathname === '/') { + throw err; + } + const baseUrl = new URL(serverUrl.origin); + logger.debug( + `[MCPOAuth] Discovery threw for path URL, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`, + { error: err }, + ); + return discoverAuthorizationServerMetadata(baseUrl, { fetchFn }); + } if (!metadata && serverUrl.pathname !== '/') { const baseUrl = new URL(serverUrl.origin); logger.debug( diff --git a/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts b/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts index b171e84d13..9545486fde 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts @@ -17,20 +17,20 @@ import * as net from 'net'; import * as http from 'http'; +import { Keyv } from 'keyv'; import { Agent } from 'undici'; +import { Types } from 'mongoose'; import { randomUUID } from 'crypto'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { Keyv } from 'keyv'; -import { Types } from 'mongoose'; import type { IUser } from '@librechat/data-schemas'; import type { Socket } from 'net'; import type * as t from '~/mcp/types'; -import { MCPInspectionFailedError } from '~/mcp/errors'; import { registryStatusCache } from '~/mcp/registry/cache/RegistryStatusCache'; import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; +import { MCPInspectionFailedError } from '~/mcp/errors'; import { FlowStateManager } from '~/flow/manager'; import { MCPConnection } from '~/mcp/connection'; import { MCPManager } from '~/mcp/MCPManager'; From 6167ce6e57f37b2c33563fe72f9c7205f9d99da3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Mar 2026 17:44:13 -0400 Subject: [PATCH 017/111] =?UTF-8?q?=F0=9F=A7=AA=20chore:=20MCP=20Reconnect?= =?UTF-8?q?=20Storm=20Follow-Up=20Fixes=20and=20Integration=20Tests=20(#12?= =?UTF-8?q?172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🧪 test: Add reconnection storm regression tests for MCPConnection Introduced a comprehensive test suite for reconnection storm scenarios, validating circuit breaker, throttling, cooldown, and timeout fixes. The tests utilize real MCP SDK transports and a StreamableHTTP server to ensure accurate behavior under rapid connect/disconnect cycles and error handling for SSE 400/405 responses. This enhances the reliability of the MCPConnection by ensuring proper handling of reconnection logic and circuit breaker functionality. * 🔧 fix: Update createUnavailableToolStub to return structured response Modified the `createUnavailableToolStub` function to return an array containing the unavailable message and a null value, enhancing the response structure. Additionally, added a debug log to skip tool creation when the result is null, improving the handling of reconnection scenarios in the MCP service. * 🧪 test: Enhance MCP tool creation tests for cache and throttle interactions Added new test cases for the `createMCPTool` function to validate the caching behavior when tools are unavailable or throttled. The tests ensure that tools are correctly cached as missing and prevent unnecessary reconnects across different users, improving the reliability of the MCP service under concurrent usage scenarios. Additionally, introduced a test for the `createMCPTools` function to verify that it returns an empty array when reconnect is throttled, ensuring proper handling of throttling logic. * 📝 docs: Update AGENTS.md with testing philosophy and guidelines Expanded the testing section in AGENTS.md to emphasize the importance of using real logic over mocks, advocating for the use of spies and real dependencies in tests. Added specific recommendations for testing with MongoDB and MCP SDK, highlighting the need to mock only uncontrollable external services. This update aims to improve testing practices and encourage more robust test implementations. * 🧪 test: Enhance reconnection storm tests with socket tracking and SSE handling Updated the reconnection storm test suite to include a new socket tracking mechanism for better resource management during tests. Improved the handling of SSE 400/405 responses by ensuring they are processed in the same branch as 404 errors, preventing unhandled cases. This enhances the reliability of the MCPConnection under rapid reconnect scenarios and ensures proper error handling. * 🔧 fix: Implement cache eviction for stale reconnect attempts and missing tools Added an `evictStale` function to manage the size of the `lastReconnectAttempts` and `missingToolCache` maps, ensuring they do not exceed a maximum cache size. This enhancement improves resource management by removing outdated entries based on a specified time-to-live (TTL), thereby optimizing the MCP service's performance during reconnection scenarios. --- AGENTS.md | 10 +- api/server/services/MCP.js | 24 +- api/server/services/MCP.spec.js | 183 ++++++ .../mcp/__tests__/reconnection-storm.test.ts | 521 ++++++++++++++++++ 4 files changed, 736 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/mcp/__tests__/reconnection-storm.test.ts diff --git a/AGENTS.md b/AGENTS.md index 23b5fc0fbb..ec44607aa7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,7 +149,15 @@ Multi-line imports count total character length across all lines. Consolidate va - Run tests from their workspace directory: `cd api && npx jest `, `cd packages/api && npx jest `, etc. - Frontend tests: `__tests__` directories alongside components; use `test/layout-test-utils` for rendering. - Cover loading, success, and error states for UI/data flows. -- Mock data-provider hooks and external dependencies. + +### Philosophy + +- **Real logic over mocks.** Exercise actual code paths with real dependencies. Mocking is a last resort. +- **Spies over mocks.** Assert that real functions are called with expected arguments and frequency without replacing underlying logic. +- **MongoDB**: use `mongodb-memory-server` for a real in-memory MongoDB instance. Test actual queries and schema validation, not mocked DB calls. +- **MCP**: use real `@modelcontextprotocol/sdk` exports for servers, transports, and tool definitions. Mirror real scenarios, don't stub SDK internals. +- Only mock what you cannot control: external HTTP APIs, rate-limited services, non-deterministic system calls. +- Heavy mocking is a code smell, not a testing strategy. --- diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 4f8cdc8195..c66eb0b6ef 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -34,12 +34,28 @@ const { reinitMCPServer } = require('./Tools/mcp'); const { getAppConfig } = require('./Config'); const { getLogStores } = require('~/cache'); +const MAX_CACHE_SIZE = 1000; const lastReconnectAttempts = new Map(); const RECONNECT_THROTTLE_MS = 10_000; const missingToolCache = new Map(); const MISSING_TOOL_TTL_MS = 10_000; +function evictStale(map, ttl) { + if (map.size <= MAX_CACHE_SIZE) { + return; + } + const now = Date.now(); + for (const [key, timestamp] of map) { + if (now - timestamp >= ttl) { + map.delete(key); + } + if (map.size <= MAX_CACHE_SIZE) { + return; + } + } +} + const unavailableMsg = "This tool's MCP server is temporarily unavailable. Please try again shortly."; @@ -49,7 +65,7 @@ const unavailableMsg = */ function createUnavailableToolStub(toolName, serverName) { const normalizedToolKey = `${toolName}${Constants.mcp_delimiter}${normalizeServerName(serverName)}`; - const _call = async () => unavailableMsg; + const _call = async () => [unavailableMsg, null]; const toolInstance = tool(_call, { schema: { type: 'object', @@ -253,6 +269,7 @@ async function reconnectServer({ return null; } lastReconnectAttempts.set(throttleKey, now); + evictStale(lastReconnectAttempts, RECONNECT_THROTTLE_MS); const runId = Constants.USE_PRELIM_RESPONSE_MESSAGE_ID; const flowId = `${user.id}:${serverName}:${Date.now()}`; @@ -373,6 +390,10 @@ async function createMCPTools({ userMCPAuthMap, streamId, }); + if (result === null) { + logger.debug(`[MCP][${serverName}] Reconnect throttled, skipping tool creation.`); + return []; + } if (!result || !result.tools) { logger.warn(`[MCP][${serverName}] Failed to reinitialize MCP server.`); return []; @@ -469,6 +490,7 @@ async function createMCPTool({ if (!toolDefinition) { missingToolCache.set(toolKey, Date.now()); + evictStale(missingToolCache, MISSING_TOOL_TTL_MS); } } diff --git a/api/server/services/MCP.spec.js b/api/server/services/MCP.spec.js index b2caebc91e..14a9ef90ed 100644 --- a/api/server/services/MCP.spec.js +++ b/api/server/services/MCP.spec.js @@ -45,6 +45,7 @@ const { getMCPSetupData, checkOAuthFlowStatus, getServerConnectionStatus, + createUnavailableToolStub, } = require('./MCP'); jest.mock('./Config', () => ({ @@ -1098,6 +1099,188 @@ describe('User parameter passing tests', () => { }); }); + describe('createUnavailableToolStub', () => { + it('should return a tool whose _call returns a valid CONTENT_AND_ARTIFACT two-tuple', async () => { + const stub = createUnavailableToolStub('myTool', 'myServer'); + // invoke() goes through langchain's base tool, which checks responseFormat. + // CONTENT_AND_ARTIFACT requires [content, artifact] — a bare string would throw: + // "Tool response format is "content_and_artifact" but the output was not a two-tuple" + const result = await stub.invoke({}); + // If we reach here without throwing, the two-tuple format is correct. + // invoke() returns the content portion of [content, artifact] as a string. + expect(result).toContain('temporarily unavailable'); + }); + }); + + describe('negative tool cache and throttle interaction', () => { + it('should cache tool as missing even when throttled (cross-user dedup)', async () => { + const mockUser = { id: 'throttle-test-user' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // First call: reconnect succeeds but tool not found + mockReinitMCPServer.mockResolvedValueOnce({ + availableTools: {}, + }); + + await createMCPTool({ + res: mockRes, + user: mockUser, + toolKey: `missing-tool${D}cache-dedup-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + // Second call within 10s for DIFFERENT tool on same server: + // reconnect is throttled (returns null), tool is still cached as missing. + // This is intentional: the cache acts as cross-user dedup since the + // throttle is per-user-per-server and can't prevent N different users + // from each triggering their own reconnect. + const result2 = await createMCPTool({ + res: mockRes, + user: mockUser, + toolKey: `other-tool${D}cache-dedup-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(result2).toBeDefined(); + expect(result2.name).toContain('other-tool'); + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + }); + + it('should prevent user B from triggering reconnect when user A already cached the tool', async () => { + const userA = { id: 'cache-user-A' }; + const userB = { id: 'cache-user-B' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // User A: real reconnect, tool not found → cached + mockReinitMCPServer.mockResolvedValueOnce({ + availableTools: {}, + }); + + await createMCPTool({ + res: mockRes, + user: userA, + toolKey: `shared-tool${D}cross-user-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + + // User B requests the SAME tool within 10s. + // The negative cache is keyed by toolKey (no user prefix), so user B + // gets a cache hit and no reconnect fires. This is the cross-user + // storm protection: without this, user B's unthrottled first request + // would trigger a second reconnect to the same server. + const result = await createMCPTool({ + res: mockRes, + user: userB, + toolKey: `shared-tool${D}cross-user-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(result).toBeDefined(); + expect(result.name).toContain('shared-tool'); + // reinitMCPServer still called only once — user B hit the cache + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + }); + + it('should prevent user B from triggering reconnect for throttle-cached tools', async () => { + const userA = { id: 'storm-user-A' }; + const userB = { id: 'storm-user-B' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // User A: real reconnect for tool-1, tool not found → cached + mockReinitMCPServer.mockResolvedValueOnce({ + availableTools: {}, + }); + + await createMCPTool({ + res: mockRes, + user: userA, + toolKey: `tool-1${D}storm-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + // User A: tool-2 on same server within 10s → throttled → cached from throttle + await createMCPTool({ + res: mockRes, + user: userA, + toolKey: `tool-2${D}storm-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + + // User B requests tool-2 — gets cache hit from the throttle-cached entry. + // Without this caching, user B would trigger a real reconnect since + // user B has their own throttle key and hasn't reconnected yet. + const result = await createMCPTool({ + res: mockRes, + user: userB, + toolKey: `tool-2${D}storm-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(result).toBeDefined(); + expect(result.name).toContain('tool-2'); + // Still only 1 real reconnect — user B was protected by the cache + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + }); + }); + + describe('createMCPTools throttle handling', () => { + it('should return empty array with debug log when reconnect is throttled', async () => { + const mockUser = { id: 'throttle-tools-user' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // First call: real reconnect + mockReinitMCPServer.mockResolvedValueOnce({ + tools: [{ name: 'tool1' }], + availableTools: { + [`tool1${D}throttle-tools-server`]: { + function: { description: 'Tool 1', parameters: {} }, + }, + }, + }); + + await createMCPTools({ + res: mockRes, + user: mockUser, + serverName: 'throttle-tools-server', + provider: 'openai', + userMCPAuthMap: {}, + }); + + // Second call within 10s — throttled + const result = await createMCPTools({ + res: mockRes, + user: mockUser, + serverName: 'throttle-tools-server', + provider: 'openai', + userMCPAuthMap: {}, + }); + + expect(result).toEqual([]); + // reinitMCPServer called only once — second was throttled + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + // Should log at debug level (not warn) for throttled case + expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Reconnect throttled')); + }); + }); + describe('User parameter integrity', () => { it('should preserve user object properties through the call chain', async () => { const complexUser = { diff --git a/packages/api/src/mcp/__tests__/reconnection-storm.test.ts b/packages/api/src/mcp/__tests__/reconnection-storm.test.ts new file mode 100644 index 0000000000..c1cf0ec5df --- /dev/null +++ b/packages/api/src/mcp/__tests__/reconnection-storm.test.ts @@ -0,0 +1,521 @@ +/** + * Reconnection storm regression tests for PR #12162. + * + * Validates circuit breaker, throttling, cooldown, and timeout fixes using real + * MCP SDK transports (no mocked stubs). A real StreamableHTTP server is spun up + * per test suite and MCPConnection talks to it through a genuine HTTP stack. + */ +import http from 'http'; +import { randomUUID } from 'crypto'; +import express from 'express'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { Socket } from 'net'; +import { OAuthReconnectionTracker } from '~/mcp/oauth/OAuthReconnectionTracker'; +import { MCPConnection } from '~/mcp/connection'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +interface TestServer { + url: string; + httpServer: http.Server; + close: () => Promise; +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +function startMCPServer(): Promise { + const app = express(); + app.use(express.json()); + + const transports: Record = {}; + + function createServer(): McpServer { + const server = new McpServer({ name: 'test-server', version: '1.0.0' }); + server.tool('echo', 'echoes input', { message: { type: 'string' } as never }, async (args) => { + const msg = (args as Record).message ?? ''; + return { content: [{ type: 'text', text: msg }] }; + }); + return server; + } + + app.all('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (sessionId && transports[sessionId]) { + await transports[sessionId].handleRequest(req, res, req.body); + return; + } + + if (!sessionId && isInitializeRequest(req.body)) { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sid) => { + transports[sid] = transport; + }, + }); + transport.onclose = () => { + const sid = transport.sessionId; + if (sid) { + delete transports[sid]; + } + }; + const server = createServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + return; + } + + if (req.method === 'GET') { + res.status(404).send('Not Found'); + return; + } + + res.status(400).json({ + jsonrpc: '2.0', + error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, + id: null, + }); + }); + + return new Promise((resolve) => { + const httpServer = app.listen(0, '127.0.0.1', () => { + const destroySockets = trackSockets(httpServer); + const addr = httpServer.address() as { port: number }; + resolve({ + url: `http://127.0.0.1:${addr.port}/mcp`, + httpServer, + close: async () => { + for (const t of Object.values(transports)) { + t.close().catch(() => {}); + } + await destroySockets(); + }, + }); + }); + }); +} + +function createConnection(serverName: string, url: string, initTimeout = 5000): MCPConnection { + return new MCPConnection({ + serverName, + serverConfig: { url, type: 'streamable-http', initTimeout } as never, + }); +} + +async function teardownConnection(conn: MCPConnection): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +afterEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (MCPConnection as any).circuitBreakers.clear(); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #2 — Circuit breaker trips after rapid connect/disconnect */ +/* cycles (5 cycles within 60s -> 30s cooldown) */ +/* ------------------------------------------------------------------ */ +describe('Fix #2: Circuit breaker stops rapid reconnect cycling', () => { + it('blocks connection after 5 rapid cycles via static circuit breaker', async () => { + const srv = await startMCPServer(); + const conn = createConnection('cycling-server', srv.url); + + let completedCycles = 0; + let breakerMessage = ''; + for (let cycle = 0; cycle < 10; cycle++) { + try { + await conn.connect(); + await teardownConnection(conn); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = false; + completedCycles++; + } catch (e) { + breakerMessage = (e as Error).message; + break; + } + } + + expect(breakerMessage).toContain('Circuit breaker is open'); + expect(completedCycles).toBeLessThanOrEqual(5); + + await srv.close(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #3 — SSE 400/405 handled in same branch as 404 */ +/* ------------------------------------------------------------------ */ +describe('Fix #3: SSE 400/405 handled in same branch as 404', () => { + it('400 with active session triggers reconnection (session lost)', async () => { + const srv = await startMCPServer(); + const conn = createConnection('sse-400', srv.url); + await conn.connect(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = true; + + const changes: string[] = []; + conn.on('connectionChange', (s: string) => changes.push(s)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transport = (conn as any).transport; + transport.onerror({ message: 'Failed to open SSE stream', code: 400 }); + + expect(changes).toContain('error'); + + await teardownConnection(conn); + await srv.close(); + }); + + it('405 with active session triggers reconnection (session lost)', async () => { + const srv = await startMCPServer(); + const conn = createConnection('sse-405', srv.url); + await conn.connect(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = true; + + const changes: string[] = []; + conn.on('connectionChange', (s: string) => changes.push(s)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transport = (conn as any).transport; + transport.onerror({ message: 'Method Not Allowed', code: 405 }); + + expect(changes).toContain('error'); + + await teardownConnection(conn); + await srv.close(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #4 — Circuit breaker state persists in static Map across */ +/* instance replacements */ +/* ------------------------------------------------------------------ */ +describe('Fix #4: Circuit breaker state persists across instance replacement', () => { + it('new MCPConnection for same serverName inherits breaker state from static Map', async () => { + const srv = await startMCPServer(); + + const conn1 = createConnection('replace', srv.url); + await conn1.connect(); + await teardownConnection(conn1); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cbAfterConn1 = (MCPConnection as any).circuitBreakers.get('replace'); + expect(cbAfterConn1).toBeDefined(); + const cyclesAfterConn1 = cbAfterConn1.cycleCount; + expect(cyclesAfterConn1).toBeGreaterThan(0); + + const conn2 = createConnection('replace', srv.url); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cbFromConn2 = (conn2 as any).getCircuitBreaker(); + expect(cbFromConn2.cycleCount).toBe(cyclesAfterConn1); + + await teardownConnection(conn2); + await srv.close(); + }); + + it('clearCooldown resets static state so explicit retry proceeds', () => { + const conn = createConnection('replace', 'http://127.0.0.1:1/mcp'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (conn as any).getCircuitBreaker(); + cb.cooldownUntil = Date.now() + 999_999; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((conn as any).isCircuitOpen()).toBe(true); + + MCPConnection.clearCooldown('replace'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((conn as any).isCircuitOpen()).toBe(false); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #5 — Dead servers now trigger circuit breaker via */ +/* recordFailedRound() in the catch path */ +/* ------------------------------------------------------------------ */ +describe('Fix #5: Dead server triggers circuit breaker', () => { + it('3 failures trigger backoff, blocking subsequent attempts before they reach the SDK', async () => { + const conn = createConnection('dead', 'http://127.0.0.1:1/mcp', 1000); + const spy = jest.spyOn(conn.client, 'connect'); + + const errors: string[] = []; + for (let i = 0; i < 5; i++) { + try { + await conn.connect(); + } catch (e) { + errors.push((e as Error).message); + } + } + + expect(spy.mock.calls.length).toBe(3); + expect(errors).toHaveLength(5); + expect(errors.filter((m) => m.includes('Circuit breaker is open'))).toHaveLength(2); + + await conn.disconnect(); + }); + + it('user B is immediately blocked when user A already tripped the breaker for the same server', async () => { + const deadUrl = 'http://127.0.0.1:1/mcp'; + + const userA = new MCPConnection({ + serverName: 'shared-dead', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-A', + }); + + for (let i = 0; i < 3; i++) { + try { + await userA.connect(); + } catch { + // expected + } + } + + const userB = new MCPConnection({ + serverName: 'shared-dead', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-B', + }); + const spyB = jest.spyOn(userB.client, 'connect'); + + let blockedMessage = ''; + try { + await userB.connect(); + } catch (e) { + blockedMessage = (e as Error).message; + } + + expect(blockedMessage).toContain('Circuit breaker is open'); + expect(spyB).toHaveBeenCalledTimes(0); + + await userA.disconnect(); + await userB.disconnect(); + }); + + it('clearCooldown after user retry unblocks all users', async () => { + const deadUrl = 'http://127.0.0.1:1/mcp'; + + const userA = new MCPConnection({ + serverName: 'shared-dead-clear', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-A', + }); + for (let i = 0; i < 3; i++) { + try { + await userA.connect(); + } catch { + // expected + } + } + + const userB = new MCPConnection({ + serverName: 'shared-dead-clear', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-B', + }); + try { + await userB.connect(); + } catch (e) { + expect((e as Error).message).toContain('Circuit breaker is open'); + } + + MCPConnection.clearCooldown('shared-dead-clear'); + + const spyB = jest.spyOn(userB.client, 'connect'); + try { + await userB.connect(); + } catch { + // expected — server is still dead + } + + expect(spyB).toHaveBeenCalledTimes(1); + + await userA.disconnect(); + await userB.disconnect(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #5b — disconnect(false) preserves cycle tracking */ +/* ------------------------------------------------------------------ */ +describe('Fix #5b: disconnect(false) preserves cycle tracking', () => { + it('connect() passes false to disconnect, which calls recordCycle()', async () => { + const srv = await startMCPServer(); + const conn = createConnection('wipe', srv.url); + const spy = jest.spyOn(conn, 'disconnect'); + + await conn.connect(); + expect(spy).toHaveBeenCalledWith(false); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (MCPConnection as any).circuitBreakers.get('wipe'); + expect(cb).toBeDefined(); + expect(cb.cycleCount).toBeGreaterThan(0); + + await teardownConnection(conn); + await srv.close(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #6 — OAuth failure uses cooldown-based retry */ +/* ------------------------------------------------------------------ */ +describe('Fix #6: OAuth failure uses cooldown-based retry', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + it('isFailed expires after first cooldown of 5 min', () => { + jest.setSystemTime(Date.now()); + const tracker = new OAuthReconnectionTracker(); + tracker.setFailed('u1', 'srv'); + + expect(tracker.isFailed('u1', 'srv')).toBe(true); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + }); + + it('progressive cooldown: 5m, 10m, 20m, 30m (capped)', () => { + jest.setSystemTime(Date.now()); + const tracker = new OAuthReconnectionTracker(); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(10 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(20 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(29 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + }); + + it('removeFailed resets attempt count so next failure starts at 5m', () => { + jest.setSystemTime(Date.now()); + const tracker = new OAuthReconnectionTracker(); + + tracker.setFailed('u1', 'srv'); + tracker.setFailed('u1', 'srv'); + tracker.setFailed('u1', 'srv'); + tracker.removeFailed('u1', 'srv'); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Integration: Circuit breaker caps rapid cycling with real transport */ +/* ------------------------------------------------------------------ */ +describe('Cascade: Circuit breaker caps rapid cycling', () => { + it('breaker trips before 10 cycles complete against a live server', async () => { + const srv = await startMCPServer(); + const conn = createConnection('cascade', srv.url); + const spy = jest.spyOn(conn.client, 'connect'); + + let completedCycles = 0; + for (let i = 0; i < 10; i++) { + try { + await conn.connect(); + await teardownConnection(conn); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = false; + completedCycles++; + } catch (e) { + if ((e as Error).message.includes('Circuit breaker is open')) { + break; + } + throw e; + } + } + + expect(completedCycles).toBeLessThanOrEqual(5); + expect(spy.mock.calls.length).toBeLessThanOrEqual(5); + + await srv.close(); + }); + + it('breaker bounds failures against a killed server', async () => { + const srv = await startMCPServer(); + const conn = createConnection('cascade-die', srv.url, 2000); + + await conn.connect(); + await teardownConnection(conn); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = false; + await srv.close(); + + let breakerTripped = false; + for (let i = 0; i < 10; i++) { + try { + await conn.connect(); + } catch (e) { + if ((e as Error).message.includes('Circuit breaker is open')) { + breakerTripped = true; + break; + } + } + } + + expect(breakerTripped).toBe(true); + }, 30_000); +}); + +/* ------------------------------------------------------------------ */ +/* Sanity: Real transport works end-to-end */ +/* ------------------------------------------------------------------ */ +describe('Sanity: Real MCP SDK transport works correctly', () => { + it('connects, lists tools, and disconnects cleanly', async () => { + const srv = await startMCPServer(); + const conn = createConnection('sanity', srv.url); + + await conn.connect(); + expect(await conn.isConnected()).toBe(true); + + const tools = await conn.fetchTools(); + expect(tools).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'echo' })])); + + await teardownConnection(conn); + await srv.close(); + }); +}); From fcb344da47cbbe51634ee4c6620598acc88e145b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Mar 2026 21:15:01 -0400 Subject: [PATCH 018/111] =?UTF-8?q?=F0=9F=9B=82=20fix:=20MCP=20OAuth=20Rac?= =?UTF-8?q?e=20Conditions,=20CSRF=20Fallback,=20and=20Token=20Expiry=20Han?= =?UTF-8?q?dling=20(#12171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Implement race conditions in MCP OAuth flow - Added connection mutex to coalesce concurrent `getUserConnection` calls, preventing multiple simultaneous attempts. - Enhanced flow state management to retry once when a flow state is missing, improving resilience against race conditions. - Introduced `ReauthenticationRequiredError` for better error handling when access tokens are expired or missing. - Updated tests to cover new race condition scenarios and ensure proper handling of OAuth flows. * fix: Stale PENDING flow detection and OAuth URL re-issuance PENDING flows in handleOAuthRequired now check createdAt age — flows older than 2 minutes are treated as stale and replaced instead of joined. Fixes the case where a leftover PENDING flow from a previous session blocks new OAuth initiation. authorizationUrl is now stored in MCPOAuthFlowMetadata so that when a second caller joins an active PENDING flow (e.g., the SSE-emitting path in ToolService), it can re-issue the URL to the user via oauthStart. * fix: CSRF fallback via active PENDING flow in OAuth callback When the OAuth callback arrives without CSRF or session cookies (common in the chat/SSE flow where cookies can't be set on streaming responses), fall back to validating that a PENDING flow exists for the flowId. This is safe because the flow was created server-side after JWT authentication and the authorization code is PKCE-protected. * test: Extract shared OAuth test server helpers Move MockKeyv, getFreePort, trackSockets, and createOAuthMCPServer into a shared helpers/oauthTestServer module. Enhance the test server with refresh token support, token rotation, metadata discovery, and dynamic client registration endpoints. Add InMemoryTokenStore for token storage tests. Refactor MCPOAuthRaceCondition.test.ts to import from shared helpers. * test: Add comprehensive MCP OAuth test modules MCPOAuthTokenStorage — 21 tests for storeTokens/getTokens with InMemoryTokenStore: encrypt/decrypt round-trips, expiry calculation, refresh callback wiring, ReauthenticationRequiredError paths. MCPOAuthFlow — 10 tests against real HTTP server: token refresh with stored client info, refresh token rotation, metadata discovery, dynamic client registration, full store/retrieve/expire/refresh lifecycle. MCPOAuthConnectionEvents — 5 tests for MCPConnection OAuth event cycle with real OAuth-gated MCP server: oauthRequired emission on 401, oauthHandled reconnection, oauthFailed rejection, token expiry detection. MCPOAuthTokenExpiry — 12 tests for the token expiry edge case: refresh success/failure paths, ReauthenticationRequiredError, PENDING flow CSRF fallback, authorizationUrl metadata storage, full re-auth cycle after refresh failure, concurrent expired token coalescing, stale PENDING flow detection. * test: Enhance MCP OAuth connection tests with cooldown reset Added a `beforeEach` hook to clear the cooldown for `MCPConnection` before each test, ensuring a clean state. Updated the race condition handling in the tests to properly clear the timeout, improving reliability in the event data retrieval process. * refactor: PENDING flow management and state recovery in MCP OAuth - Introduced a constant `PENDING_STALE_MS` to define the age threshold for PENDING flows, improving the handling of stale flows. - Updated the logic in `MCPConnectionFactory` and `FlowStateManager` to check the age of PENDING flows before joining or reusing them. - Modified the `completeFlow` method to return false when the flow state is deleted, ensuring graceful handling of race conditions. - Enhanced tests to validate the new behavior and ensure robustness against state recovery issues. * refactor: MCP OAuth flow management and testing - Updated the `completeFlow` method to log warnings when a tool flow state is not found during completion, improving error handling. - Introduced a new `normalizeExpiresAt` function to standardize expiration timestamp handling across the application. - Refactored token expiration checks in `MCPConnectionFactory` to utilize the new normalization function, ensuring consistent behavior. - Added a comprehensive test suite for OAuth callback CSRF fallback logic, validating the handling of PENDING flows and their staleness. - Enhanced existing tests to cover new expiration normalization logic and ensure robust flow state management. * test: Add CSRF fallback tests for active PENDING flows in MCP OAuth - Introduced new tests to validate CSRF fallback behavior when a fresh PENDING flow exists without cookies, ensuring successful OAuth callback handling. - Added scenarios to reject requests when no PENDING flow exists, when only a COMPLETED flow is present, and when a PENDING flow is stale, enhancing the robustness of flow state management. - Improved overall test coverage for OAuth callback logic, reinforcing the handling of CSRF validation failures. * chore: imports order * refactor: Update UserConnectionManager to conditionally manage pending connections - Modified the logic in `UserConnectionManager` to only set pending connections if `forceNew` is false, preventing unnecessary overwrites. - Adjusted the cleanup process to ensure pending connections are only deleted when not forced, enhancing connection management efficiency. * refactor: MCP OAuth flow state management - Introduced a new method `storeStateMapping` in `MCPOAuthHandler` to securely map the OAuth state parameter to the flow ID, improving callback resolution and security against forgery. - Updated the OAuth initiation and callback handling in `mcp.js` to utilize the new state mapping functionality, ensuring robust flow management. - Refactored `MCPConnectionFactory` to store state mappings during flow initialization, enhancing the integrity of the OAuth process. - Adjusted comments to clarify the purpose of state parameters in authorization URLs, reinforcing code readability. * refactor: MCPConnection with OAuth recovery handling - Added `oauthRecovery` flag to manage OAuth recovery state during connection attempts. - Introduced `decrementCycleCount` method to reduce the circuit breaker's cycle count upon successful reconnection after OAuth recovery. - Updated connection logic to reset the `oauthRecovery` flag after handling OAuth, improving state management and connection reliability. * chore: Add debug logging for OAuth recovery cycle count decrement - Introduced a debug log statement in the `MCPConnection` class to track the decrement of the cycle count after a successful reconnection during OAuth recovery. - This enhancement improves observability and aids in troubleshooting connection issues related to OAuth recovery. * test: Add OAuth recovery cycle management tests - Introduced new tests for the OAuth recovery cycle in `MCPConnection`, validating the decrement of cycle counts after successful reconnections. - Added scenarios to ensure that the cycle count is not decremented on OAuth failures, enhancing the robustness of connection management. - Improved test coverage for OAuth reconnect scenarios, ensuring reliable behavior under various conditions. * feat: Implement circuit breaker configuration in MCP - Added circuit breaker settings to `.env.example` for max cycles, cycle window, and cooldown duration. - Refactored `MCPConnection` to utilize the new configuration values from `mcpConfig`, enhancing circuit breaker management. - Improved code maintainability by centralizing circuit breaker parameters in the configuration file. * refactor: Update decrementCycleCount method for circuit breaker management - Changed the visibility of the `decrementCycleCount` method in `MCPConnection` from private to public static, allowing it to be called with a server name parameter. - Updated calls to `decrementCycleCount` in `MCPConnectionFactory` to use the new static method, improving clarity and consistency in circuit breaker management during connection failures and OAuth recovery. - Enhanced the handling of circuit breaker state by ensuring the method checks for the existence of the circuit breaker before decrementing the cycle count. * refactor: cycle count decrement on tool listing failure - Added a call to `MCPConnection.decrementCycleCount` in the `MCPConnectionFactory` to handle cases where unauthenticated tool listing fails, improving circuit breaker management. - This change ensures that the cycle count is decremented appropriately, maintaining the integrity of the connection recovery process. * refactor: Update circuit breaker configuration and logic - Enhanced circuit breaker settings in `.env.example` to include new parameters for failed rounds and backoff strategies. - Refactored `MCPConnection` to utilize the updated configuration values from `mcpConfig`, improving circuit breaker management. - Updated tests to reflect changes in circuit breaker logic, ensuring accurate validation of connection behavior under rapid reconnect scenarios. * feat: Implement state mapping deletion in MCP flow management - Added a new method `deleteStateMapping` in `MCPOAuthHandler` to remove orphaned state mappings when a flow is replaced, preventing old authorization URLs from resolving after a flow restart. - Updated `MCPConnectionFactory` to call `deleteStateMapping` during flow cleanup, ensuring proper management of OAuth states. - Enhanced test coverage for state mapping functionality to validate the new deletion logic. --- .env.example | 21 + api/server/routes/__tests__/mcp.spec.js | 121 ++++ api/server/routes/mcp.js | 68 +- packages/api/jest.config.mjs | 1 + packages/api/package.json | 4 +- packages/api/src/flow/manager.ts | 79 +-- packages/api/src/mcp/MCPConnectionFactory.ts | 105 ++- packages/api/src/mcp/UserConnectionManager.ts | 86 ++- .../__tests__/MCPConnectionFactory.test.ts | 4 +- .../__tests__/MCPOAuthCSRFFallback.test.ts | 232 +++++++ .../MCPOAuthConnectionEvents.test.ts | 268 +++++++ .../src/mcp/__tests__/MCPOAuthFlow.test.ts | 538 ++++++++++++++ .../__tests__/MCPOAuthRaceCondition.test.ts | 516 ++++++++++++++ .../mcp/__tests__/MCPOAuthTokenExpiry.test.ts | 654 ++++++++++++++++++ .../__tests__/MCPOAuthTokenStorage.test.ts | 544 +++++++++++++++ .../mcp/__tests__/helpers/oauthTestServer.ts | 449 ++++++++++++ .../mcp/__tests__/reconnection-storm.test.ts | 175 ++++- packages/api/src/mcp/connection.ts | 43 +- packages/api/src/mcp/mcpConfig.ts | 14 + packages/api/src/mcp/oauth/handler.ts | 46 +- packages/api/src/mcp/oauth/tokens.ts | 24 +- packages/api/src/mcp/oauth/types.ts | 1 + 22 files changed, 3865 insertions(+), 128 deletions(-) create mode 100644 packages/api/src/mcp/__tests__/MCPOAuthCSRFFallback.test.ts create mode 100644 packages/api/src/mcp/__tests__/MCPOAuthConnectionEvents.test.ts create mode 100644 packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts create mode 100644 packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts create mode 100644 packages/api/src/mcp/__tests__/MCPOAuthTokenExpiry.test.ts create mode 100644 packages/api/src/mcp/__tests__/MCPOAuthTokenStorage.test.ts create mode 100644 packages/api/src/mcp/__tests__/helpers/oauthTestServer.ts diff --git a/.env.example b/.env.example index b851749baf..e746737ea4 100644 --- a/.env.example +++ b/.env.example @@ -850,3 +850,24 @@ OPENWEATHER_API_KEY= # Skip code challenge method validation (e.g., for AWS Cognito that supports S256 but doesn't advertise it) # When set to true, forces S256 code challenge even if not advertised in .well-known/openid-configuration # MCP_SKIP_CODE_CHALLENGE_CHECK=false + +# Circuit breaker: max connect/disconnect cycles before tripping (per server) +# MCP_CB_MAX_CYCLES=7 + +# Circuit breaker: sliding window (ms) for counting cycles +# MCP_CB_CYCLE_WINDOW_MS=45000 + +# Circuit breaker: cooldown (ms) after the cycle breaker trips +# MCP_CB_CYCLE_COOLDOWN_MS=15000 + +# Circuit breaker: max consecutive failed connection rounds before backoff +# MCP_CB_MAX_FAILED_ROUNDS=3 + +# Circuit breaker: sliding window (ms) for counting failed rounds +# MCP_CB_FAILED_WINDOW_MS=120000 + +# Circuit breaker: base backoff (ms) after failed round threshold is reached +# MCP_CB_BASE_BACKOFF_MS=30000 + +# Circuit breaker: max backoff cap (ms) for exponential backoff +# MCP_CB_MAX_BACKOFF_MS=300000 diff --git a/api/server/routes/__tests__/mcp.spec.js b/api/server/routes/__tests__/mcp.spec.js index e87fcf8f15..009b602604 100644 --- a/api/server/routes/__tests__/mcp.spec.js +++ b/api/server/routes/__tests__/mcp.spec.js @@ -32,6 +32,9 @@ jest.mock('@librechat/api', () => { getFlowState: jest.fn(), completeOAuthFlow: jest.fn(), generateFlowId: jest.fn(), + resolveStateToFlowId: jest.fn(async (state) => state), + storeStateMapping: jest.fn(), + deleteStateMapping: jest.fn(), }, MCPTokenStorage: { storeTokens: jest.fn(), @@ -180,7 +183,10 @@ describe('MCP Routes', () => { MCPOAuthHandler.initiateOAuthFlow.mockResolvedValue({ authorizationUrl: 'https://oauth.example.com/auth', flowId: 'test-user-id:test-server', + flowMetadata: { state: 'random-state-value' }, }); + MCPOAuthHandler.storeStateMapping.mockResolvedValue(); + mockFlowManager.initFlow = jest.fn().mockResolvedValue(); const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', @@ -367,6 +373,121 @@ describe('MCP Routes', () => { expect(response.headers.location).toBe(`${basePath}/oauth/error?error=invalid_state`); }); + describe('CSRF fallback via active PENDING flow', () => { + it('should proceed when a fresh PENDING flow exists and no cookies are present', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + createdAt: Date.now(), + }), + completeFlow: jest.fn().mockResolvedValue(true), + deleteFlow: jest.fn().mockResolvedValue(true), + }; + const mockFlowState = { + serverName: 'test-server', + userId: 'test-user-id', + metadata: {}, + clientInfo: {}, + codeVerifier: 'test-verifier', + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); + MCPOAuthHandler.completeOAuthFlow.mockResolvedValue({ + access_token: 'test-token', + }); + MCPTokenStorage.storeTokens.mockResolvedValue(); + mockRegistryInstance.getServerConfig.mockResolvedValue({}); + + const mockMcpManager = { + getUserConnection: jest.fn().mockResolvedValue({ + fetchTools: jest.fn().mockResolvedValue([]), + }), + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + require('~/config').getOAuthReconnectionManager.mockReturnValue({ + clearReconnection: jest.fn(), + }); + require('~/server/services/Config/mcp').updateMCPServerTools.mockResolvedValue(); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toContain(`${basePath}/oauth/success`); + }); + + it('should reject when no PENDING flow exists and no cookies are present', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue(null), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + + it('should reject when only a COMPLETED flow exists (not PENDING)', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'COMPLETED', + createdAt: Date.now(), + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + + it('should reject when PENDING flow is stale (older than PENDING_STALE_MS)', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + createdAt: Date.now() - 3 * 60 * 1000, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + }); + it('should handle OAuth callback successfully', async () => { // mockRegistryInstance is defined at the top of the file const mockFlowManager = { diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 2db8c2c462..0afac81192 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -13,6 +13,7 @@ const { MCPOAuthHandler, MCPTokenStorage, setOAuthSession, + PENDING_STALE_MS, getUserMCPAuthMap, validateOAuthCsrf, OAUTH_CSRF_COOKIE, @@ -91,7 +92,11 @@ router.get('/:serverName/oauth/initiate', requireJwtAuth, setOAuthSession, async } const oauthHeaders = await getOAuthHeaders(serverName, userId); - const { authorizationUrl, flowId: oauthFlowId } = await MCPOAuthHandler.initiateOAuthFlow( + const { + authorizationUrl, + flowId: oauthFlowId, + flowMetadata, + } = await MCPOAuthHandler.initiateOAuthFlow( serverName, serverUrl, userId, @@ -101,6 +106,7 @@ router.get('/:serverName/oauth/initiate', requireJwtAuth, setOAuthSession, async logger.debug('[MCP OAuth] OAuth flow initiated', { oauthFlowId, authorizationUrl }); + await MCPOAuthHandler.storeStateMapping(flowMetadata.state, oauthFlowId, flowManager); setOAuthCsrfCookie(res, oauthFlowId, OAUTH_CSRF_COOKIE_PATH); res.redirect(authorizationUrl); } catch (error) { @@ -143,30 +149,52 @@ router.get('/:serverName/oauth/callback', async (req, res) => { return res.redirect(`${basePath}/oauth/error?error=missing_state`); } - const flowId = state; - logger.debug('[MCP OAuth] Using flow ID from state', { flowId }); + const flowsCache = getLogStores(CacheKeys.FLOWS); + const flowManager = getFlowStateManager(flowsCache); + + const flowId = await MCPOAuthHandler.resolveStateToFlowId(state, flowManager); + if (!flowId) { + logger.error('[MCP OAuth] Could not resolve state to flow ID', { state }); + return res.redirect(`${basePath}/oauth/error?error=invalid_state`); + } + logger.debug('[MCP OAuth] Resolved flow ID from state', { flowId }); const flowParts = flowId.split(':'); if (flowParts.length < 2 || !flowParts[0] || !flowParts[1]) { - logger.error('[MCP OAuth] Invalid flow ID format in state', { flowId }); + logger.error('[MCP OAuth] Invalid flow ID format', { flowId }); return res.redirect(`${basePath}/oauth/error?error=invalid_state`); } const [flowUserId] = flowParts; - if ( - !validateOAuthCsrf(req, res, flowId, OAUTH_CSRF_COOKIE_PATH) && - !validateOAuthSession(req, flowUserId) - ) { - logger.error('[MCP OAuth] CSRF validation failed: no valid CSRF or session cookie', { - flowId, - hasCsrfCookie: !!req.cookies?.[OAUTH_CSRF_COOKIE], - hasSessionCookie: !!req.cookies?.[OAUTH_SESSION_COOKIE], - }); - return res.redirect(`${basePath}/oauth/error?error=csrf_validation_failed`); + + const hasCsrf = validateOAuthCsrf(req, res, flowId, OAUTH_CSRF_COOKIE_PATH); + const hasSession = !hasCsrf && validateOAuthSession(req, flowUserId); + let hasActiveFlow = false; + if (!hasCsrf && !hasSession) { + const pendingFlow = await flowManager.getFlowState(flowId, 'mcp_oauth'); + const pendingAge = pendingFlow?.createdAt ? Date.now() - pendingFlow.createdAt : Infinity; + hasActiveFlow = pendingFlow?.status === 'PENDING' && pendingAge < PENDING_STALE_MS; + if (hasActiveFlow) { + logger.debug( + '[MCP OAuth] CSRF/session cookies absent, validating via active PENDING flow', + { + flowId, + }, + ); + } } - const flowsCache = getLogStores(CacheKeys.FLOWS); - const flowManager = getFlowStateManager(flowsCache); + if (!hasCsrf && !hasSession && !hasActiveFlow) { + logger.error( + '[MCP OAuth] CSRF validation failed: no valid CSRF cookie, session cookie, or active flow', + { + flowId, + hasCsrfCookie: !!req.cookies?.[OAUTH_CSRF_COOKIE], + hasSessionCookie: !!req.cookies?.[OAUTH_SESSION_COOKIE], + }, + ); + return res.redirect(`${basePath}/oauth/error?error=csrf_validation_failed`); + } logger.debug('[MCP OAuth] Getting flow state for flowId: ' + flowId); const flowState = await MCPOAuthHandler.getFlowState(flowId, flowManager); @@ -281,7 +309,13 @@ router.get('/:serverName/oauth/callback', async (req, res) => { const toolFlowId = flowState.metadata?.toolFlowId; if (toolFlowId) { logger.debug('[MCP OAuth] Completing tool flow', { toolFlowId }); - await flowManager.completeFlow(toolFlowId, 'mcp_oauth', tokens); + const completed = await flowManager.completeFlow(toolFlowId, 'mcp_oauth', tokens); + if (!completed) { + logger.warn( + '[MCP OAuth] Tool flow state not found during completion — waiter will time out', + { toolFlowId }, + ); + } } /** Redirect to success page with flowId and serverName */ diff --git a/packages/api/jest.config.mjs b/packages/api/jest.config.mjs index 530150a7fa..df9cf6bcc2 100644 --- a/packages/api/jest.config.mjs +++ b/packages/api/jest.config.mjs @@ -7,6 +7,7 @@ export default { '\\.dev\\.ts$', '\\.helper\\.ts$', '\\.helper\\.d\\.ts$', + '/__tests__/helpers/', ], coverageReporters: ['text', 'cobertura'], testResultsProcessor: 'jest-junit', diff --git a/packages/api/package.json b/packages/api/package.json index e4ca4ef3c5..46587797a5 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -18,8 +18,8 @@ "build:dev": "npm run clean && NODE_ENV=development rollup -c --bundleConfigAsCjs", "build:watch": "NODE_ENV=development rollup -c -w --bundleConfigAsCjs", "build:watch:prod": "rollup -c -w --bundleConfigAsCjs", - "test": "jest --coverage --watch --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.\"", - "test:ci": "jest --coverage --ci --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.\"", + "test": "jest --coverage --watch --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.|__tests__/helpers/\"", + "test:ci": "jest --coverage --ci --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.|__tests__/helpers/\"", "test:cache-integration:core": "jest --testPathPatterns=\"src/cache/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false", "test:cache-integration:cluster": "jest --testPathPatterns=\"src/cluster/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false --runInBand", "test:cache-integration:mcp": "jest --testPathPatterns=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false", diff --git a/packages/api/src/flow/manager.ts b/packages/api/src/flow/manager.ts index 4f9023a3d7..b68b9edb7a 100644 --- a/packages/api/src/flow/manager.ts +++ b/packages/api/src/flow/manager.ts @@ -3,6 +3,18 @@ import { logger } from '@librechat/data-schemas'; import type { StoredDataNoRaw } from 'keyv'; import type { FlowState, FlowMetadata, FlowManagerOptions } from './types'; +export const PENDING_STALE_MS = 2 * 60 * 1000; + +const SECONDS_THRESHOLD = 1e10; + +/** + * Normalizes an expiration timestamp to milliseconds. + * Timestamps below 10 billion are assumed to be in seconds (valid until ~2286). + */ +export function normalizeExpiresAt(timestamp: number): number { + return timestamp < SECONDS_THRESHOLD ? timestamp * 1000 : timestamp; +} + export class FlowStateManager { private keyv: Keyv; private ttl: number; @@ -45,32 +57,8 @@ export class FlowStateManager { return `${type}:${flowId}`; } - /** - * Normalizes an expiration timestamp to milliseconds. - * Detects whether the input is in seconds or milliseconds based on magnitude. - * Timestamps below 10 billion are assumed to be in seconds (valid until ~2286). - * @param timestamp - The expiration timestamp (in seconds or milliseconds) - * @returns The timestamp normalized to milliseconds - */ - private normalizeExpirationTimestamp(timestamp: number): number { - const SECONDS_THRESHOLD = 1e10; - if (timestamp < SECONDS_THRESHOLD) { - return timestamp * 1000; - } - return timestamp; - } - - /** - * Checks if a flow's token has expired based on its expires_at field - * @param flowState - The flow state to check - * @returns true if the token has expired, false otherwise (including if no expires_at exists) - */ private isTokenExpired(flowState: FlowState | undefined): boolean { - if (!flowState?.result) { - return false; - } - - if (typeof flowState.result !== 'object') { + if (!flowState?.result || typeof flowState.result !== 'object') { return false; } @@ -79,13 +67,11 @@ export class FlowStateManager { } const expiresAt = (flowState.result as { expires_at: unknown }).expires_at; - if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)) { return false; } - const normalizedExpiresAt = this.normalizeExpirationTimestamp(expiresAt); - return normalizedExpiresAt < Date.now(); + return normalizeExpiresAt(expiresAt) < Date.now(); } /** @@ -149,6 +135,8 @@ export class FlowStateManager { let elapsedTime = 0; let isCleanedUp = false; let intervalId: NodeJS.Timeout | null = null; + let missingStateRetried = false; + let isRetrying = false; // Cleanup function to avoid duplicate cleanup const cleanup = () => { @@ -188,16 +176,29 @@ export class FlowStateManager { } intervalId = setInterval(async () => { - if (isCleanedUp) return; + if (isCleanedUp || isRetrying) return; try { - const flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; + let flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; if (!flowState) { - cleanup(); - logger.error(`[${flowKey}] Flow state not found`); - reject(new Error(`${type} Flow state not found`)); - return; + if (!missingStateRetried) { + missingStateRetried = true; + isRetrying = true; + logger.warn( + `[${flowKey}] Flow state not found, retrying once after 500ms (race recovery)`, + ); + await new Promise((r) => setTimeout(r, 500)); + flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; + isRetrying = false; + } + + if (!flowState) { + cleanup(); + logger.error(`[${flowKey}] Flow state not found after retry`); + reject(new Error(`${type} Flow state not found`)); + return; + } } if (signal?.aborted) { @@ -251,10 +252,10 @@ export class FlowStateManager { const flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; if (!flowState) { - logger.warn('[FlowStateManager] Cannot complete flow - flow state not found', { - flowId, - type, - }); + logger.warn( + '[FlowStateManager] Flow state not found during completion — cannot recover metadata, skipping', + { flowId, type }, + ); return false; } @@ -297,7 +298,7 @@ export class FlowStateManager { async isFlowStale( flowId: string, type: string, - staleThresholdMs: number = 2 * 60 * 1000, + staleThresholdMs: number = PENDING_STALE_MS, ): Promise<{ isStale: boolean; age: number; status?: string }> { const flowKey = this.getFlowKey(flowId, type); const flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; diff --git a/packages/api/src/mcp/MCPConnectionFactory.ts b/packages/api/src/mcp/MCPConnectionFactory.ts index 03131b659b..0fc86e0315 100644 --- a/packages/api/src/mcp/MCPConnectionFactory.ts +++ b/packages/api/src/mcp/MCPConnectionFactory.ts @@ -2,11 +2,11 @@ import { logger } from '@librechat/data-schemas'; import type { OAuthClientInformation } from '@modelcontextprotocol/sdk/shared/auth.js'; import type { Tool } from '@modelcontextprotocol/sdk/types.js'; import type { TokenMethods } from '@librechat/data-schemas'; -import type { MCPOAuthTokens, OAuthMetadata } from '~/mcp/oauth'; +import type { MCPOAuthTokens, OAuthMetadata, MCPOAuthFlowMetadata } from '~/mcp/oauth'; import type { FlowStateManager } from '~/flow/manager'; -import type { FlowMetadata } from '~/flow/types'; import type * as t from './types'; -import { MCPTokenStorage, MCPOAuthHandler } from '~/mcp/oauth'; +import { MCPTokenStorage, MCPOAuthHandler, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { PENDING_STALE_MS, normalizeExpiresAt } from '~/flow/manager'; import { sanitizeUrlForLogging } from './utils'; import { withTimeout } from '~/utils/promise'; import { MCPConnection } from './connection'; @@ -104,6 +104,7 @@ export class MCPConnectionFactory { return { tools, connection, oauthRequired: false, oauthUrl: null }; } } catch { + MCPConnection.decrementCycleCount(this.serverName); logger.debug( `${this.logPrefix} [Discovery] Connection failed, attempting unauthenticated tool listing`, ); @@ -125,7 +126,9 @@ export class MCPConnectionFactory { } return { tools, connection: null, oauthRequired, oauthUrl }; } + MCPConnection.decrementCycleCount(this.serverName); } catch (listError) { + MCPConnection.decrementCycleCount(this.serverName); logger.debug(`${this.logPrefix} [Discovery] Unauthenticated tool listing failed:`, listError); } @@ -265,6 +268,10 @@ export class MCPConnectionFactory { if (tokens) logger.info(`${this.logPrefix} Loaded OAuth tokens`); return tokens; } catch (error) { + if (error instanceof ReauthenticationRequiredError) { + logger.info(`${this.logPrefix} ${error.message}, will trigger OAuth flow`); + return null; + } logger.debug(`${this.logPrefix} No existing tokens found or error loading tokens`, error); return null; } @@ -306,11 +313,21 @@ export class MCPConnectionFactory { const existingFlow = await this.flowManager!.getFlowState(flowId, 'mcp_oauth'); if (existingFlow?.status === 'PENDING') { + const pendingAge = existingFlow.createdAt + ? Date.now() - existingFlow.createdAt + : Infinity; + + if (pendingAge < PENDING_STALE_MS) { + logger.debug( + `${this.logPrefix} Recent PENDING OAuth flow exists (${Math.round(pendingAge / 1000)}s old), skipping new initiation`, + ); + connection.emit('oauthFailed', new Error('OAuth flow initiated - return early')); + return; + } + logger.debug( - `${this.logPrefix} PENDING OAuth flow already exists, skipping new initiation`, + `${this.logPrefix} Found stale PENDING OAuth flow (${Math.round(pendingAge / 1000)}s old), will replace`, ); - connection.emit('oauthFailed', new Error('OAuth flow initiated - return early')); - return; } const { @@ -326,11 +343,17 @@ export class MCPConnectionFactory { ); if (existingFlow) { + const oldState = (existingFlow.metadata as MCPOAuthFlowMetadata)?.state; await this.flowManager!.deleteFlow(newFlowId, 'mcp_oauth'); + if (oldState) { + await MCPOAuthHandler.deleteStateMapping(oldState, this.flowManager!); + } } // Store flow state BEFORE redirecting so the callback can find it - await this.flowManager!.initFlow(newFlowId, 'mcp_oauth', flowMetadata); + const metadataWithUrl = { ...flowMetadata, authorizationUrl }; + await this.flowManager!.initFlow(newFlowId, 'mcp_oauth', metadataWithUrl); + await MCPOAuthHandler.storeStateMapping(flowMetadata.state, newFlowId, this.flowManager!); // Start monitoring in background — createFlow will find the existing PENDING state // written by initFlow above, so metadata arg is unused (pass {} to make that explicit) @@ -495,11 +518,75 @@ export class MCPConnectionFactory { const existingFlow = await this.flowManager.getFlowState(flowId, 'mcp_oauth'); if (existingFlow) { + const flowMeta = existingFlow.metadata as MCPOAuthFlowMetadata | undefined; + + if (existingFlow.status === 'PENDING') { + const pendingAge = existingFlow.createdAt + ? Date.now() - existingFlow.createdAt + : Infinity; + + if (pendingAge < PENDING_STALE_MS) { + logger.debug( + `${this.logPrefix} Found recent PENDING OAuth flow (${Math.round(pendingAge / 1000)}s old), joining instead of creating new one`, + ); + + const storedAuthUrl = flowMeta?.authorizationUrl; + if (storedAuthUrl && typeof this.oauthStart === 'function') { + logger.info( + `${this.logPrefix} Re-issuing stored authorization URL to caller while joining PENDING flow`, + ); + await this.oauthStart(storedAuthUrl); + } + + const tokens = await this.flowManager.createFlow(flowId, 'mcp_oauth', {}, this.signal); + if (typeof this.oauthEnd === 'function') { + await this.oauthEnd(); + } + logger.info( + `${this.logPrefix} Joined existing OAuth flow completed for ${this.serverName}`, + ); + return { + tokens, + clientInfo: flowMeta?.clientInfo, + metadata: flowMeta?.metadata, + }; + } + + logger.debug( + `${this.logPrefix} Found stale PENDING OAuth flow (${Math.round(pendingAge / 1000)}s old), will delete and start fresh`, + ); + } + + if (existingFlow.status === 'COMPLETED') { + const completedAge = existingFlow.completedAt + ? Date.now() - existingFlow.completedAt + : Infinity; + const cachedTokens = existingFlow.result as MCPOAuthTokens | null | undefined; + const isTokenExpired = + cachedTokens?.expires_at != null && + normalizeExpiresAt(cachedTokens.expires_at) < Date.now(); + + if (completedAge <= PENDING_STALE_MS && cachedTokens !== undefined && !isTokenExpired) { + logger.debug( + `${this.logPrefix} Found non-stale COMPLETED OAuth flow, reusing cached tokens`, + ); + return { + tokens: cachedTokens, + clientInfo: flowMeta?.clientInfo, + metadata: flowMeta?.metadata, + }; + } + } + logger.debug( `${this.logPrefix} Found existing OAuth flow (status: ${existingFlow.status}), cleaning up to start fresh`, ); try { + const oldState = flowMeta?.state; await this.flowManager.deleteFlow(flowId, 'mcp_oauth'); + if (oldState) { + await MCPOAuthHandler.deleteStateMapping(oldState, this.flowManager); + } } catch (error) { logger.warn(`${this.logPrefix} Failed to clean up existing OAuth flow`, error); } @@ -519,7 +606,9 @@ export class MCPConnectionFactory { ); // Store flow state BEFORE redirecting so the callback can find it - await this.flowManager.initFlow(newFlowId, 'mcp_oauth', flowMetadata as FlowMetadata); + const metadataWithUrl = { ...flowMetadata, authorizationUrl }; + await this.flowManager.initFlow(newFlowId, 'mcp_oauth', metadataWithUrl); + await MCPOAuthHandler.storeStateMapping(flowMetadata.state, newFlowId, this.flowManager); if (typeof this.oauthStart === 'function') { logger.info(`${this.logPrefix} OAuth flow started, issued authorization URL to user`); diff --git a/packages/api/src/mcp/UserConnectionManager.ts b/packages/api/src/mcp/UserConnectionManager.ts index 0828b1720a..76523fc0fc 100644 --- a/packages/api/src/mcp/UserConnectionManager.ts +++ b/packages/api/src/mcp/UserConnectionManager.ts @@ -1,10 +1,10 @@ import { logger } from '@librechat/data-schemas'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; -import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; -import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; -import { MCPConnection } from './connection'; import type * as t from './types'; +import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; +import { MCPConnection } from './connection'; import { mcpConfig } from './mcpConfig'; /** @@ -21,6 +21,8 @@ export abstract class UserConnectionManager { protected userConnections: Map> = new Map(); /** Last activity timestamp for users (not per server) */ protected userLastActivity: Map = new Map(); + /** In-flight connection promises keyed by `userId:serverName` — coalesces concurrent attempts */ + protected pendingConnections: Map> = new Map(); /** Updates the last activity timestamp for a user */ protected updateUserLastActivity(userId: string): void { @@ -31,29 +33,64 @@ export abstract class UserConnectionManager { ); } - /** Gets or creates a connection for a specific user */ - public async getUserConnection({ - serverName, - forceNew, - user, - flowManager, - customUserVars, - requestBody, - tokenMethods, - oauthStart, - oauthEnd, - signal, - returnOnOAuth = false, - connectionTimeout, - }: { - serverName: string; - forceNew?: boolean; - } & Omit): Promise { + /** Gets or creates a connection for a specific user, coalescing concurrent attempts */ + public async getUserConnection( + opts: { + serverName: string; + forceNew?: boolean; + } & Omit, + ): Promise { + const { serverName, forceNew, user } = opts; const userId = user?.id; if (!userId) { throw new McpError(ErrorCode.InvalidRequest, `[MCP] User object missing id property`); } + const lockKey = `${userId}:${serverName}`; + + if (!forceNew) { + const pending = this.pendingConnections.get(lockKey); + if (pending) { + logger.debug(`[MCP][User: ${userId}][${serverName}] Joining in-flight connection attempt`); + return pending; + } + } + + const connectionPromise = this.createUserConnectionInternal(opts, userId); + + if (!forceNew) { + this.pendingConnections.set(lockKey, connectionPromise); + } + + try { + return await connectionPromise; + } finally { + if (!forceNew && this.pendingConnections.get(lockKey) === connectionPromise) { + this.pendingConnections.delete(lockKey); + } + } + } + + private async createUserConnectionInternal( + { + serverName, + forceNew, + user, + flowManager, + customUserVars, + requestBody, + tokenMethods, + oauthStart, + oauthEnd, + signal, + returnOnOAuth = false, + connectionTimeout, + }: { + serverName: string; + forceNew?: boolean; + } & Omit, + userId: string, + ): Promise { if (await this.appConnections!.has(serverName)) { throw new McpError( ErrorCode.InvalidRequest, @@ -188,6 +225,7 @@ export abstract class UserConnectionManager { /** Disconnects and removes a specific user connection */ public async disconnectUserConnection(userId: string, serverName: string): Promise { + this.pendingConnections.delete(`${userId}:${serverName}`); const userMap = this.userConnections.get(userId); const connection = userMap?.get(serverName); if (connection) { @@ -215,6 +253,12 @@ export abstract class UserConnectionManager { ); } await Promise.allSettled(disconnectPromises); + // Clean up any pending connection promises for this user + for (const key of this.pendingConnections.keys()) { + if (key.startsWith(`${userId}:`)) { + this.pendingConnections.delete(key); + } + } // Ensure user activity timestamp is removed this.userLastActivity.delete(userId); logger.info(`[MCP][User: ${userId}] All connections processed for disconnection.`); diff --git a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts index de18e27e89..bceb23b246 100644 --- a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts +++ b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts @@ -275,7 +275,7 @@ describe('MCPConnectionFactory', () => { expect(mockFlowManager.initFlow).toHaveBeenCalledWith( 'flow123', 'mcp_oauth', - mockFlowData.flowMetadata, + expect.objectContaining(mockFlowData.flowMetadata), ); const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0]; const oauthStartCallOrder = (oauthOptions.oauthStart as jest.Mock).mock @@ -550,7 +550,7 @@ describe('MCPConnectionFactory', () => { expect(mockFlowManager.initFlow).toHaveBeenCalledWith( 'flow123', 'mcp_oauth', - mockFlowData.flowMetadata, + expect.objectContaining(mockFlowData.flowMetadata), ); const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0]; const oauthStartCallOrder = (oauthOptions.oauthStart as jest.Mock).mock diff --git a/packages/api/src/mcp/__tests__/MCPOAuthCSRFFallback.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthCSRFFallback.test.ts new file mode 100644 index 0000000000..cdba06cf8d --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthCSRFFallback.test.ts @@ -0,0 +1,232 @@ +/** + * Tests for the OAuth callback CSRF fallback logic. + * + * The callback route validates requests via three mechanisms (in order): + * 1. CSRF cookie (HMAC-based, set during initiate) + * 2. Session cookie (bound to authenticated userId) + * 3. Active PENDING flow in FlowStateManager (fallback for SSE/chat flows) + * + * This suite tests mechanism 3 — the PENDING flow fallback — including + * staleness enforcement and rejection of non-PENDING flows. + * + * These tests exercise the validation functions directly for fast, + * focused coverage. Route-level integration tests using supertest + * are in api/server/routes/__tests__/mcp.spec.js ("CSRF fallback + * via active PENDING flow" describe block). + */ + +import { Keyv } from 'keyv'; +import { FlowStateManager, PENDING_STALE_MS } from '~/flow/manager'; +import type { Request, Response } from 'express'; +import { + generateOAuthCsrfToken, + OAUTH_SESSION_COOKIE, + validateOAuthSession, + OAUTH_CSRF_COOKIE, + validateOAuthCsrf, +} from '~/oauth/csrf'; +import { MockKeyv } from './helpers/oauthTestServer'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +const CSRF_COOKIE_PATH = '/api/mcp'; + +function makeReq(cookies: Record = {}): Request { + return { cookies } as unknown as Request; +} + +function makeRes(): Response { + const res = { + clearCookie: jest.fn(), + } as unknown as Response; + return res; +} + +/** + * Replicate the callback route's three-tier validation logic. + * Returns which mechanism (if any) authorized the request. + */ +async function validateCallback( + req: Request, + res: Response, + flowId: string, + flowManager: FlowStateManager, +): Promise<'csrf' | 'session' | 'pendingFlow' | false> { + const flowUserId = flowId.split(':')[0]; + + const hasCsrf = validateOAuthCsrf(req, res, flowId, CSRF_COOKIE_PATH); + if (hasCsrf) { + return 'csrf'; + } + + const hasSession = validateOAuthSession(req, flowUserId); + if (hasSession) { + return 'session'; + } + + const pendingFlow = await flowManager.getFlowState(flowId, 'mcp_oauth'); + const pendingAge = pendingFlow?.createdAt ? Date.now() - pendingFlow.createdAt : Infinity; + if (pendingFlow?.status === 'PENDING' && pendingAge < PENDING_STALE_MS) { + return 'pendingFlow'; + } + + return false; +} + +describe('OAuth Callback CSRF Fallback', () => { + let flowManager: FlowStateManager; + + beforeEach(() => { + process.env.JWT_SECRET = 'test-secret-for-csrf'; + const store = new MockKeyv(); + flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 300000, ci: true }); + }); + + afterEach(() => { + delete process.env.JWT_SECRET; + jest.clearAllMocks(); + }); + + describe('CSRF cookie validation (mechanism 1)', () => { + it('should accept valid CSRF cookie', async () => { + const flowId = 'user1:test-server'; + const csrfToken = generateOAuthCsrfToken(flowId, 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_CSRF_COOKIE]: csrfToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('csrf'); + }); + + it('should reject invalid CSRF cookie', async () => { + const flowId = 'user1:test-server'; + const req = makeReq({ [OAUTH_CSRF_COOKIE]: 'wrong-token-value' }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + }); + + describe('Session cookie validation (mechanism 2)', () => { + it('should accept valid session cookie when CSRF is absent', async () => { + const flowId = 'user1:test-server'; + const sessionToken = generateOAuthCsrfToken('user1', 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_SESSION_COOKIE]: sessionToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('session'); + }); + }); + + describe('PENDING flow fallback (mechanism 3)', () => { + it('should accept when a fresh PENDING flow exists and no cookies are present', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('pendingFlow'); + }); + + it('should reject when no PENDING flow, no CSRF cookie, and no session cookie', async () => { + const flowId = 'user1:test-server'; + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should reject when only a COMPLETED flow exists (not PENDING)', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + await flowManager.completeFlow(flowId, 'mcp_oauth', { access_token: 'tok' } as never); + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should reject when only a FAILED flow exists', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + await flowManager.failFlow(flowId, 'mcp_oauth', 'some error'); + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should reject when PENDING flow is stale (older than PENDING_STALE_MS)', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + // Artificially age the flow past the staleness threshold + const store = (flowManager as unknown as { keyv: { get: (k: string) => Promise } }) + .keyv; + const flowState = (await store.get(`mcp_oauth:${flowId}`)) as { createdAt: number }; + flowState.createdAt = Date.now() - PENDING_STALE_MS - 1000; + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should accept PENDING flow that is just under the staleness threshold', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + // Flow was just created, well under threshold + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('pendingFlow'); + }); + }); + + describe('Priority ordering', () => { + it('should prefer CSRF cookie over PENDING flow', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + const csrfToken = generateOAuthCsrfToken(flowId, 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_CSRF_COOKIE]: csrfToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('csrf'); + }); + + it('should prefer session cookie over PENDING flow when CSRF is absent', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + const sessionToken = generateOAuthCsrfToken('user1', 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_SESSION_COOKIE]: sessionToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('session'); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthConnectionEvents.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthConnectionEvents.test.ts new file mode 100644 index 0000000000..4e168d00f3 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthConnectionEvents.test.ts @@ -0,0 +1,268 @@ +/** + * Tests for MCPConnection OAuth event cycle against a real OAuth-gated MCP server. + * + * Verifies: oauthRequired emission on 401, oauthHandled reconnection, + * oauthFailed rejection, timeout behavior, and token expiry mid-session. + */ + +import { MCPConnection } from '~/mcp/connection'; +import { createOAuthMCPServer } from './helpers/oauthTestServer'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0, USER_CONNECTION_IDLE_TIMEOUT: 30 * 60 * 1000 }, +})); + +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) { + return; + } + try { + await conn.disconnect(); + } catch { + // Ignore disconnect errors during cleanup + } +} + +async function exchangeCodeForToken(serverUrl: string): Promise { + const authRes = await fetch(`${serverUrl}authorize?redirect_uri=http://localhost&state=test`, { + redirect: 'manual', + }); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${serverUrl}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const data = (await tokenRes.json()) as { access_token: string }; + return data.access_token; +} + +describe('MCPConnection OAuth Events — Real Server', () => { + let server: OAuthTestServer; + let connection: MCPConnection | null = null; + + beforeEach(() => { + MCPConnection.clearCooldown('test-server'); + }); + + afterEach(async () => { + await safeDisconnect(connection); + connection = null; + if (server) { + await server.close(); + } + jest.clearAllMocks(); + }); + + describe('oauthRequired event', () => { + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + it('should emit oauthRequired when connecting without a token', async () => { + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { type: 'streamable-http', url: server.url }, + userId: 'user-1', + }); + + const oauthRequiredPromise = new Promise<{ + serverName: string; + error: Error; + serverUrl?: string; + userId?: string; + }>((resolve) => { + connection!.on('oauthRequired', (data) => { + resolve( + data as { + serverName: string; + error: Error; + serverUrl?: string; + userId?: string; + }, + ); + }); + }); + + // Connection will fail with 401, emitting oauthRequired + const connectPromise = connection.connect().catch(() => { + // Expected to fail since no one handles oauthRequired + }); + + let raceTimer: NodeJS.Timeout | undefined; + const eventData = await Promise.race([ + oauthRequiredPromise, + new Promise((_, reject) => { + raceTimer = setTimeout( + () => reject(new Error('Timed out waiting for oauthRequired')), + 10000, + ); + }), + ]).finally(() => clearTimeout(raceTimer)); + + expect(eventData.serverName).toBe('test-server'); + expect(eventData.error).toBeDefined(); + + // Emit oauthFailed to unblock connect() + connection.emit('oauthFailed', new Error('test cleanup')); + await connectPromise.catch(() => undefined); + }); + + it('should not emit oauthRequired when connecting with a valid token', async () => { + const accessToken = await exchangeCodeForToken(server.url); + + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { type: 'streamable-http', url: server.url }, + userId: 'user-1', + oauthTokens: { + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens, + }); + + let oauthFired = false; + connection.on('oauthRequired', () => { + oauthFired = true; + }); + + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + expect(oauthFired).toBe(false); + }); + }); + + describe('oauthHandled reconnection', () => { + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + it('should succeed on retry after oauthHandled provides valid tokens', async () => { + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { + type: 'streamable-http', + url: server.url, + initTimeout: 15000, + }, + userId: 'user-1', + }); + + // First connect fails with 401 → oauthRequired fires + let oauthFired = false; + connection.on('oauthRequired', () => { + oauthFired = true; + connection!.emit('oauthFailed', new Error('Will retry with tokens')); + }); + + // First attempt fails as expected + await expect(connection.connect()).rejects.toThrow(); + expect(oauthFired).toBe(true); + + // Now set valid tokens and reconnect + const accessToken = await exchangeCodeForToken(server.url); + connection.setOAuthTokens({ + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + }); + }); + + describe('oauthFailed rejection', () => { + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + it('should reject connect() when oauthFailed is emitted', async () => { + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { + type: 'streamable-http', + url: server.url, + initTimeout: 15000, + }, + userId: 'user-1', + }); + + connection.on('oauthRequired', () => { + connection!.emit('oauthFailed', new Error('User denied OAuth')); + }); + + await expect(connection.connect()).rejects.toThrow(); + }); + }); + + describe('Token expiry during session', () => { + it('should detect expired token on reconnect and emit oauthRequired', async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 1000 }); + + const accessToken = await exchangeCodeForToken(server.url); + + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { + type: 'streamable-http', + url: server.url, + initTimeout: 15000, + }, + userId: 'user-1', + oauthTokens: { + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens, + }); + + // Initial connect should succeed + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + await connection.disconnect(); + + // Wait for token to expire + await new Promise((r) => setTimeout(r, 1200)); + + // Reconnect should trigger oauthRequired since token is expired on the server + let oauthFired = false; + connection.on('oauthRequired', () => { + oauthFired = true; + connection!.emit('oauthFailed', new Error('Will retry with fresh token')); + }); + + // First reconnect fails with 401 → oauthRequired + await expect(connection.connect()).rejects.toThrow(); + expect(oauthFired).toBe(true); + + // Get fresh token and reconnect + const newToken = await exchangeCodeForToken(server.url); + connection.setOAuthTokens({ + access_token: newToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts new file mode 100644 index 0000000000..8437177c86 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts @@ -0,0 +1,538 @@ +/** + * OAuth flow tests against a real HTTP server. + * + * Tests MCPOAuthHandler.refreshOAuthTokens and MCPTokenStorage lifecycle + * using a real test OAuth server (not mocked SDK functions). + */ + +import { createHash } from 'crypto'; +import { Keyv } from 'keyv'; +import { MCPTokenStorage, MCPOAuthHandler } from '~/mcp/oauth'; +import { FlowStateManager } from '~/flow/manager'; +import { createOAuthMCPServer, MockKeyv, InMemoryTokenStore } from './helpers/oauthTestServer'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +describe('MCP OAuth Flow — Real HTTP Server', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Token refresh with real server', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should refresh tokens with stored client info via real /token endpoint', async () => { + // First get initial tokens + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + refresh_token: string; + }; + + expect(initial.refresh_token).toBeDefined(); + + // Register a client so we have clientInfo + const regRes = await fetch(`${server.url}register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ redirect_uris: ['http://localhost/callback'] }), + }); + const clientInfo = (await regRes.json()) as { + client_id: string; + client_secret: string; + }; + + // Refresh tokens using the real endpoint + const refreshed = await MCPOAuthHandler.refreshOAuthTokens( + initial.refresh_token, + { + serverName: 'test-server', + serverUrl: server.url, + clientInfo: { + ...clientInfo, + redirect_uris: ['http://localhost/callback'], + }, + }, + {}, + { + token_url: `${server.url}token`, + client_id: clientInfo.client_id, + client_secret: clientInfo.client_secret, + token_exchange_method: 'DefaultPost', + }, + ); + + expect(refreshed.access_token).toBeDefined(); + expect(refreshed.access_token).not.toBe(initial.access_token); + expect(refreshed.token_type).toBe('Bearer'); + expect(refreshed.obtained_at).toBeDefined(); + }); + + it('should get new refresh token when server rotates', async () => { + const rotatingServer = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + rotateRefreshTokens: true, + }); + + try { + const code = await rotatingServer.getAuthCode(); + const tokenRes = await fetch(`${rotatingServer.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + refresh_token: string; + }; + + const refreshed = await MCPOAuthHandler.refreshOAuthTokens( + initial.refresh_token, + { + serverName: 'test-server', + serverUrl: rotatingServer.url, + }, + {}, + { + token_url: `${rotatingServer.url}token`, + client_id: 'anon', + token_exchange_method: 'DefaultPost', + }, + ); + + expect(refreshed.access_token).not.toBe(initial.access_token); + expect(refreshed.refresh_token).toBeDefined(); + expect(refreshed.refresh_token).not.toBe(initial.refresh_token); + } finally { + await rotatingServer.close(); + } + }); + + it('should fail refresh with invalid refresh token', async () => { + await expect( + MCPOAuthHandler.refreshOAuthTokens( + 'invalid-refresh-token', + { + serverName: 'test-server', + serverUrl: server.url, + }, + {}, + { + token_url: `${server.url}token`, + client_id: 'anon', + token_exchange_method: 'DefaultPost', + }, + ), + ).rejects.toThrow(); + }); + }); + + describe('OAuth server metadata discovery', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ issueRefreshTokens: true }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should expose /.well-known/oauth-authorization-server', async () => { + const res = await fetch(`${server.url}.well-known/oauth-authorization-server`); + expect(res.status).toBe(200); + + const metadata = (await res.json()) as { + authorization_endpoint: string; + token_endpoint: string; + registration_endpoint: string; + grant_types_supported: string[]; + }; + + expect(metadata.authorization_endpoint).toContain('/authorize'); + expect(metadata.token_endpoint).toContain('/token'); + expect(metadata.registration_endpoint).toContain('/register'); + expect(metadata.grant_types_supported).toContain('authorization_code'); + expect(metadata.grant_types_supported).toContain('refresh_token'); + }); + + it('should not advertise refresh_token grant when disabled', async () => { + const noRefreshServer = await createOAuthMCPServer({ + issueRefreshTokens: false, + }); + try { + const res = await fetch(`${noRefreshServer.url}.well-known/oauth-authorization-server`); + const metadata = (await res.json()) as { grant_types_supported: string[] }; + expect(metadata.grant_types_supported).not.toContain('refresh_token'); + } finally { + await noRefreshServer.close(); + } + }); + }); + + describe('Dynamic client registration', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should register a client via /register endpoint', async () => { + const res = await fetch(`${server.url}register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + redirect_uris: ['http://localhost/callback'], + }), + }); + + expect(res.status).toBe(200); + const client = (await res.json()) as { + client_id: string; + client_secret: string; + redirect_uris: string[]; + }; + + expect(client.client_id).toBeDefined(); + expect(client.client_secret).toBeDefined(); + expect(client.redirect_uris).toEqual(['http://localhost/callback']); + expect(server.registeredClients.has(client.client_id)).toBe(true); + }); + }); + + describe('End-to-End: store, retrieve, expire, refresh cycle', () => { + it('should perform full token lifecycle with real server', async () => { + const server = await createOAuthMCPServer({ + tokenTTLMs: 1000, + issueRefreshTokens: true, + }); + const tokenStore = new InMemoryTokenStore(); + + try { + // 1. Get initial tokens via auth code exchange + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // 2. Store tokens + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'test-srv', + tokens: initial, + createToken: tokenStore.createToken, + }); + + // 3. Retrieve — should succeed + const valid = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }); + expect(valid).not.toBeNull(); + expect(valid!.access_token).toBe(initial.access_token); + expect(valid!.refresh_token).toBe(initial.refresh_token); + + // 4. Wait for expiry + await new Promise((r) => setTimeout(r, 1200)); + + // 5. Retrieve again — should trigger refresh via callback + const refreshCallback = async (refreshToken: string): Promise => { + const refreshRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + + if (!refreshRes.ok) { + throw new Error(`Refresh failed: ${refreshRes.status}`); + } + + const data = (await refreshRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token?: string; + }; + + return { + ...data, + obtained_at: Date.now(), + expires_at: Date.now() + data.expires_in * 1000, + }; + }; + + const refreshed = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(refreshed).not.toBeNull(); + expect(refreshed!.access_token).not.toBe(initial.access_token); + + // 6. Verify the refreshed token works against the server + const mcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${refreshed!.access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + expect(mcpRes.status).toBe(200); + } finally { + await server.close(); + } + }); + }); + + describe('completeOAuthFlow via FlowStateManager', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ issueRefreshTokens: true }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should exchange auth code and complete flow in FlowStateManager', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'test-user:test-server'; + const code = await server.getAuthCode(); + + // Initialize the flow with metadata the handler needs + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverUrl: server.url, + clientInfo: { + client_id: 'test-client', + redirect_uris: ['http://localhost/callback'], + }, + codeVerifier: 'test-verifier', + metadata: { + token_endpoint: `${server.url}token`, + token_endpoint_auth_methods_supported: ['client_secret_post'], + }, + }); + + // The SDK's exchangeAuthorization wants full OAuth metadata, + // so we'll test the token exchange directly instead of going through + // completeOAuthFlow (which requires full SDK-compatible metadata) + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + const tokens = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token?: string; + }; + + const mcpTokens: MCPOAuthTokens = { + ...tokens, + obtained_at: Date.now(), + expires_at: Date.now() + tokens.expires_in * 1000, + }; + + // Complete the flow + const completed = await flowManager.completeFlow(flowId, 'mcp_oauth', mcpTokens); + expect(completed).toBe(true); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('COMPLETED'); + expect(state?.result?.access_token).toBe(tokens.access_token); + }); + + it('should fail flow when authorization code is invalid', async () => { + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=authorization_code&code=invalid-code', + }); + + expect(tokenRes.status).toBe(400); + const body = (await tokenRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + + it('should fail when authorization code is reused', async () => { + const code = await server.getAuthCode(); + + // First exchange succeeds + const firstRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + expect(firstRes.status).toBe(200); + + // Second exchange fails + const secondRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + expect(secondRes.status).toBe(400); + const body = (await secondRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + }); + + describe('PKCE verification', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + afterEach(async () => { + await server.close(); + }); + + function generatePKCE(): { verifier: string; challenge: string } { + const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; + const challenge = createHash('sha256').update(verifier).digest('base64url'); + return { verifier, challenge }; + } + + it('should accept valid code_verifier matching code_challenge', async () => { + const { verifier, challenge } = generatePKCE(); + + const authRes = await fetch( + `${server.url}authorize?redirect_uri=http://localhost&state=test&code_challenge=${challenge}&code_challenge_method=S256`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}&code_verifier=${verifier}`, + }); + + expect(tokenRes.status).toBe(200); + const data = (await tokenRes.json()) as { access_token: string }; + expect(data.access_token).toBeDefined(); + }); + + it('should reject wrong code_verifier', async () => { + const { challenge } = generatePKCE(); + + const authRes = await fetch( + `${server.url}authorize?redirect_uri=http://localhost&state=test&code_challenge=${challenge}&code_challenge_method=S256`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}&code_verifier=wrong-verifier`, + }); + + expect(tokenRes.status).toBe(400); + const body = (await tokenRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + + it('should reject missing code_verifier when code_challenge was provided', async () => { + const { challenge } = generatePKCE(); + + const authRes = await fetch( + `${server.url}authorize?redirect_uri=http://localhost&state=test&code_challenge=${challenge}&code_challenge_method=S256`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + expect(tokenRes.status).toBe(400); + const body = (await tokenRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + + it('should still accept codes without PKCE when no code_challenge was provided', async () => { + const code = await server.getAuthCode(); + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + expect(tokenRes.status).toBe(200); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts new file mode 100644 index 0000000000..85febb3ece --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts @@ -0,0 +1,516 @@ +/** + * Tests for MCP OAuth race condition fixes: + * + * 1. Connection mutex coalesces concurrent getUserConnection() calls + * 2. PENDING OAuth flows are reused, not deleted + * 3. No-refresh-token expiry throws ReauthenticationRequiredError + * 4. completeFlow recovers when flow state was deleted by a race + * 5. monitorFlow retries once when flow state disappears mid-poll + */ + +import { Keyv } from 'keyv'; +import { logger } from '@librechat/data-schemas'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; +import { MCPTokenStorage, MCPOAuthHandler, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { MockKeyv, createOAuthMCPServer } from './helpers/oauthTestServer'; +import { FlowStateManager } from '~/flow/manager'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0, USER_CONNECTION_IDLE_TIMEOUT: 30 * 60 * 1000 }, +})); + +const mockLogger = logger as jest.Mocked; + +describe('MCP OAuth Race Condition Fixes', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Fix 1: Connection mutex coalesces concurrent attempts', () => { + it('should return the same pending promise for concurrent getUserConnection calls', async () => { + const { UserConnectionManager } = await import('~/mcp/UserConnectionManager'); + + class TestManager extends UserConnectionManager { + public createCallCount = 0; + + getPendingConnections() { + return this.pendingConnections; + } + } + + const manager = new TestManager(); + + const mockConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn().mockResolvedValue(undefined), + isStale: jest.fn().mockReturnValue(false), + }; + + const mockAppConnections = { has: jest.fn().mockResolvedValue(false) }; + manager.appConnections = mockAppConnections as never; + + const mockConfig = { + type: 'streamable-http', + url: 'http://localhost:9999/', + updatedAt: undefined, + dbId: undefined, + }; + + jest + .spyOn( + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('~/mcp/registry/MCPServersRegistry').MCPServersRegistry, + 'getInstance', + ) + .mockReturnValue({ + getServerConfig: jest.fn().mockResolvedValue(mockConfig), + shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + }); + + const { MCPConnectionFactory } = await import('~/mcp/MCPConnectionFactory'); + const createSpy = jest.spyOn(MCPConnectionFactory, 'create').mockImplementation(async () => { + manager.createCallCount++; + await new Promise((r) => setTimeout(r, 100)); + return mockConnection as never; + }); + + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + const user = { id: 'user-1' }; + const opts = { + serverName: 'test-server', + user: user as never, + flowManager: flowManager as never, + }; + + const [conn1, conn2, conn3] = await Promise.all([ + manager.getUserConnection(opts), + manager.getUserConnection(opts), + manager.getUserConnection(opts), + ]); + + expect(conn1).toBe(conn2); + expect(conn2).toBe(conn3); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(manager.createCallCount).toBe(1); + + createSpy.mockRestore(); + }); + + it('should not coalesce when forceNew is true', async () => { + const { UserConnectionManager } = await import('~/mcp/UserConnectionManager'); + + class TestManager extends UserConnectionManager {} + + const manager = new TestManager(); + + let callCount = 0; + const makeConnection = () => ({ + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn().mockResolvedValue(undefined), + isStale: jest.fn().mockReturnValue(false), + }); + + const mockAppConnections = { has: jest.fn().mockResolvedValue(false) }; + manager.appConnections = mockAppConnections as never; + + const mockConfig = { + type: 'streamable-http', + url: 'http://localhost:9999/', + updatedAt: undefined, + dbId: undefined, + }; + + jest + .spyOn( + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('~/mcp/registry/MCPServersRegistry').MCPServersRegistry, + 'getInstance', + ) + .mockReturnValue({ + getServerConfig: jest.fn().mockResolvedValue(mockConfig), + shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + }); + + const { MCPConnectionFactory } = await import('~/mcp/MCPConnectionFactory'); + jest.spyOn(MCPConnectionFactory, 'create').mockImplementation(async () => { + callCount++; + await new Promise((r) => setTimeout(r, 50)); + return makeConnection() as never; + }); + + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + const user = { id: 'user-2' }; + + const [conn1, conn2] = await Promise.all([ + manager.getUserConnection({ + serverName: 'test-server', + forceNew: true, + user: user as never, + flowManager: flowManager as never, + }), + manager.getUserConnection({ + serverName: 'test-server', + forceNew: true, + user: user as never, + flowManager: flowManager as never, + }), + ]); + + expect(callCount).toBe(2); + expect(conn1).not.toBe(conn2); + }); + }); + + describe('Fix 2: PENDING flow is reused, not deleted', () => { + it('should join an existing PENDING flow via createFlow instead of deleting it', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'test-flow-pending'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + clientInfo: { client_id: 'test-client' }, + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('PENDING'); + + const deleteSpy = jest.spyOn(flowManager, 'deleteFlow'); + + const monitorPromise = flowManager.createFlow(flowId, 'mcp_oauth', {}); + + await new Promise((r) => setTimeout(r, 500)); + + await flowManager.completeFlow(flowId, 'mcp_oauth', { + access_token: 'test-token', + token_type: 'Bearer', + } as never); + + const result = await monitorPromise; + expect(result).toEqual( + expect.objectContaining({ access_token: 'test-token', token_type: 'Bearer' }), + ); + expect(deleteSpy).not.toHaveBeenCalled(); + + deleteSpy.mockRestore(); + }); + + it('should delete and recreate FAILED flows', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'test-flow-failed'; + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + await flowManager.failFlow(flowId, 'mcp_oauth', 'previous error'); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('FAILED'); + + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + + const afterDelete = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(afterDelete).toBeUndefined(); + }); + }); + + describe('Fix 3: completeFlow handles deleted state gracefully', () => { + it('should return false when state was deleted by race', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'race-deleted-flow'; + + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + + const stateBeforeComplete = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(stateBeforeComplete).toBeUndefined(); + + const result = await flowManager.completeFlow(flowId, 'mcp_oauth', { + access_token: 'recovered-token', + token_type: 'Bearer', + } as never); + + expect(result).toBe(false); + + const stateAfterComplete = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(stateAfterComplete).toBeUndefined(); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('cannot recover metadata'), + expect.any(Object), + ); + }); + + it('should reject monitorFlow when state is deleted and not recoverable', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'monitor-retry-flow'; + + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + + const monitorPromise = flowManager.createFlow(flowId, 'mcp_oauth', {}); + + await new Promise((r) => setTimeout(r, 500)); + + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + + await expect(monitorPromise).rejects.toThrow('Flow state not found'); + }); + }); + + describe('State mapping cleanup on flow replacement', () => { + it('should delete old state mapping when a flow is replaced', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'user1:test-server'; + const oldState = 'old-random-state-abc123'; + const newState = 'new-random-state-xyz789'; + + // Simulate initial flow with state mapping + await flowManager.initFlow(flowId, 'mcp_oauth', { state: oldState }); + await MCPOAuthHandler.storeStateMapping(oldState, flowId, flowManager); + + // Old state should resolve + const resolvedBefore = await MCPOAuthHandler.resolveStateToFlowId(oldState, flowManager); + expect(resolvedBefore).toBe(flowId); + + // Replace the flow: delete old, create new, clean up old state mapping + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + await MCPOAuthHandler.deleteStateMapping(oldState, flowManager); + await flowManager.initFlow(flowId, 'mcp_oauth', { state: newState }); + await MCPOAuthHandler.storeStateMapping(newState, flowId, flowManager); + + // Old state should no longer resolve + const resolvedOld = await MCPOAuthHandler.resolveStateToFlowId(oldState, flowManager); + expect(resolvedOld).toBeNull(); + + // New state should resolve + const resolvedNew = await MCPOAuthHandler.resolveStateToFlowId(newState, flowManager); + expect(resolvedNew).toBe(flowId); + }); + }); + + describe('Fix 4: ReauthenticationRequiredError for no-refresh-token', () => { + it('should throw ReauthenticationRequiredError when access token expired and no refresh token', async () => { + const expiredDate = new Date(Date.now() - 60000); + + const findToken = jest.fn().mockImplementation(async (filter: { type?: string }) => { + if (filter.type === 'mcp_oauth') { + return { + token: 'enc:expired-access-token', + expiresAt: expiredDate, + createdAt: new Date(Date.now() - 120000), + }; + } + if (filter.type === 'mcp_oauth_refresh') { + return null; + } + return null; + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }), + ).rejects.toThrow('Re-authentication required'); + }); + + it('should throw ReauthenticationRequiredError when access token is missing and no refresh token', async () => { + const findToken = jest.fn().mockResolvedValue(null); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should not throw when access token is valid', async () => { + const futureDate = new Date(Date.now() + 3600000); + + const findToken = jest.fn().mockImplementation(async (filter: { type?: string }) => { + if (filter.type === 'mcp_oauth') { + return { + token: 'enc:valid-access-token', + expiresAt: futureDate, + createdAt: new Date(), + }; + } + if (filter.type === 'mcp_oauth_refresh') { + return null; + } + return null; + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }); + + expect(result).not.toBeNull(); + expect(result?.access_token).toBe('valid-access-token'); + }); + }); + + describe('E2E: OAuth-gated MCP server with no refresh tokens', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should start OAuth-gated MCP server that validates Bearer tokens', async () => { + const res = await fetch(server.url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'initialize', id: 1 }), + }); + + expect(res.status).toBe(401); + const body = (await res.json()) as { error: string }; + expect(body.error).toBe('invalid_token'); + }); + + it('should issue tokens via authorization code exchange with no refresh token', async () => { + const authRes = await fetch(`${server.url}authorize?redirect_uri=http://localhost&state=s1`, { + redirect: 'manual', + }); + + expect(authRes.status).toBe(302); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code'); + expect(code).toBeTruthy(); + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + expect(tokenRes.status).toBe(200); + const tokenBody = (await tokenRes.json()) as { + access_token: string; + token_type: string; + refresh_token?: string; + }; + expect(tokenBody.access_token).toBeTruthy(); + expect(tokenBody.token_type).toBe('Bearer'); + expect(tokenBody.refresh_token).toBeUndefined(); + }); + + it('should allow MCP requests with valid Bearer token', async () => { + const authRes = await fetch(`${server.url}authorize?redirect_uri=http://localhost&state=s1`, { + redirect: 'manual', + }); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code'); + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + const { access_token } = (await tokenRes.json()) as { access_token: string }; + + const mcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + + expect(mcpRes.status).toBe(200); + }); + + it('should reject expired tokens with 401', async () => { + const shortTTLServer = await createOAuthMCPServer({ tokenTTLMs: 500 }); + + try { + const authRes = await fetch( + `${shortTTLServer.url}authorize?redirect_uri=http://localhost&state=s1`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code'); + + const tokenRes = await fetch(`${shortTTLServer.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + const { access_token } = (await tokenRes.json()) as { access_token: string }; + + await new Promise((r) => setTimeout(r, 600)); + + const mcpRes = await fetch(shortTTLServer.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', id: 2 }), + }); + + expect(mcpRes.status).toBe(401); + } finally { + await shortTTLServer.close(); + } + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthTokenExpiry.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthTokenExpiry.test.ts new file mode 100644 index 0000000000..986ac4c8b4 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthTokenExpiry.test.ts @@ -0,0 +1,654 @@ +/** + * Tests for MCP OAuth token expiry → re-authentication scenarios. + * + * Reproduces the edge case where: + * 1. Tokens are stored (access + refresh) + * 2. Access token expires + * 3. Refresh attempt fails (server rejects/revokes refresh token) + * 4. System must fall back to full OAuth re-auth via handleOAuthRequired + * 5. The CSRF cookie may be absent (chat/SSE flow), so the PENDING flow fallback is needed + * + * Also tests the happy path: access token expired but refresh succeeds. + */ + +import { Keyv } from 'keyv'; +import { logger } from '@librechat/data-schemas'; +import { FlowStateManager, PENDING_STALE_MS } from '~/flow/manager'; +import { MCPTokenStorage, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { MockKeyv, InMemoryTokenStore, createOAuthMCPServer } from './helpers/oauthTestServer'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +describe('MCP OAuth Token Expiry Scenarios', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Access token expired + refresh token available + refresh succeeds', () => { + let server: OAuthTestServer; + let tokenStore: InMemoryTokenStore; + + beforeEach(async () => { + server = await createOAuthMCPServer({ + tokenTTLMs: 500, + issueRefreshTokens: true, + }); + tokenStore = new InMemoryTokenStore(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should refresh expired access token via real /token endpoint', async () => { + // Get initial tokens from real server + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // Store expired access token directly (bypassing storeTokens' expiresIn clamping) + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: `enc:${initial.access_token}`, + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: `enc:${initial.refresh_token}`, + expiresIn: 86400, + }); + + const refreshCallback = async (refreshToken: string): Promise => { + const res = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + if (!res.ok) { + throw new Error(`Refresh failed: ${res.status}`); + } + const data = (await res.json()) as { + access_token: string; + token_type: string; + expires_in: number; + }; + return { + ...data, + obtained_at: Date.now(), + expires_at: Date.now() + data.expires_in * 1000, + }; + }; + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(result).not.toBeNull(); + expect(result!.access_token).not.toBe(initial.access_token); + + // Verify the refreshed token works against the server + const mcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${result!.access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + expect(mcpRes.status).toBe(200); + }); + }); + + describe('Access token expired + refresh token rejected by server', () => { + let tokenStore: InMemoryTokenStore; + + beforeEach(() => { + tokenStore = new InMemoryTokenStore(); + }); + + it('should return null when refresh token is rejected (invalid_grant)', async () => { + const server = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + + try { + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // Store expired access token directly + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: `enc:${initial.access_token}`, + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: `enc:${initial.refresh_token}`, + expiresIn: 86400, + }); + + // Simulate server revoking the refresh token + server.issuedRefreshTokens.clear(); + + const refreshCallback = async (refreshToken: string): Promise => { + const res = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + if (!res.ok) { + const body = (await res.json()) as { error: string }; + throw new Error(`Token refresh failed: ${body.error}`); + } + const data = (await res.json()) as MCPOAuthTokens; + return data; + }; + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(result).toBeNull(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to refresh tokens'), + expect.any(Error), + ); + } finally { + await server.close(); + } + }); + + it('should return null when refresh endpoint returns unauthorized_client', async () => { + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: 'enc:some-refresh-token', + expiresIn: 86400, + }); + + const refreshCallback = jest + .fn() + .mockRejectedValue(new Error('unauthorized_client: client not authorized for refresh')); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(result).toBeNull(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('does not support refresh tokens'), + ); + }); + }); + + describe('Access token expired + NO refresh token → ReauthenticationRequiredError', () => { + let tokenStore: InMemoryTokenStore; + + beforeEach(() => { + tokenStore = new InMemoryTokenStore(); + }); + + it('should throw ReauthenticationRequiredError when no refresh token stored', async () => { + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should throw ReauthenticationRequiredError with correct reason for expired token', async () => { + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }), + ).rejects.toThrow('access token expired'); + }); + + it('should throw ReauthenticationRequiredError with correct reason for missing token', async () => { + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }), + ).rejects.toThrow('access token missing'); + }); + }); + + describe('PENDING flow fallback for CSRF-less OAuth callbacks', () => { + it('should allow OAuth completion when PENDING flow exists (simulating chat/SSE path)', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'user1:test-server'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + userId: 'user1', + serverUrl: 'https://example.com', + state: 'test-state', + authorizationUrl: 'https://example.com/authorize?state=user1:test-server', + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('PENDING'); + + const tokens: MCPOAuthTokens = { + access_token: 'new-access-token', + token_type: 'Bearer', + refresh_token: 'new-refresh-token', + obtained_at: Date.now(), + expires_at: Date.now() + 3600000, + }; + + const completed = await flowManager.completeFlow(flowId, 'mcp_oauth', tokens); + expect(completed).toBe(true); + + const completedState = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(completedState?.status).toBe('COMPLETED'); + expect((completedState?.result as MCPOAuthTokens | undefined)?.access_token).toBe( + 'new-access-token', + ); + }); + + it('should store authorizationUrl in flow metadata for re-issuance', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'user1:test-server'; + const authUrl = 'https://auth.example.com/authorize?client_id=abc&state=user1:test-server'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + userId: 'user1', + serverUrl: 'https://example.com', + state: 'test-state', + authorizationUrl: authUrl, + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect((state?.metadata as Record)?.authorizationUrl).toBe(authUrl); + }); + }); + + describe('Full token expiry → refresh failure → re-auth flow', () => { + let server: OAuthTestServer; + let tokenStore: InMemoryTokenStore; + + beforeEach(async () => { + server = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + tokenStore = new InMemoryTokenStore(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should go through full cycle: get tokens → expire → refresh fails → re-auth needed', async () => { + // Step 1: Get initial tokens + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // Step 2: Store tokens with valid expiry first + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'test-srv', + tokens: initial, + createToken: tokenStore.createToken, + }); + + // Step 3: Verify tokens work + const validResult = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }); + expect(validResult).not.toBeNull(); + expect(validResult!.access_token).toBe(initial.access_token); + + // Step 4: Simulate token expiry by directly updating the stored token's expiresAt + await tokenStore.updateToken({ userId: 'u1', identifier: 'mcp:test-srv' }, { expiresIn: -1 }); + + // Step 5: Revoke refresh token on server side (simulating server-side revocation) + server.issuedRefreshTokens.clear(); + + // Step 6: Try to get tokens — refresh should fail, return null + const refreshCallback = async (refreshToken: string): Promise => { + const res = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + if (!res.ok) { + const body = (await res.json()) as { error: string }; + throw new Error(`Refresh failed: ${body.error}`); + } + const data = (await res.json()) as MCPOAuthTokens; + return data; + }; + + const expiredResult = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + // Refresh failed → returns null → triggers OAuth re-auth flow + expect(expiredResult).toBeNull(); + + // Step 7: Simulate the re-auth flow via FlowStateManager + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + const flowId = 'u1:test-srv'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-srv', + userId: 'u1', + serverUrl: server.url, + state: 'test-state', + authorizationUrl: `${server.url}authorize?state=${flowId}`, + }); + + // Step 8: Get a new auth code and exchange for tokens (simulating user re-auth) + const newCode = await server.getAuthCode(); + const newTokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${newCode}`, + }); + const newTokens = (await newTokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token?: string; + }; + + // Step 9: Complete the flow + const mcpTokens: MCPOAuthTokens = { + ...newTokens, + obtained_at: Date.now(), + expires_at: Date.now() + newTokens.expires_in * 1000, + }; + await flowManager.completeFlow(flowId, 'mcp_oauth', mcpTokens); + + // Step 10: Store the new tokens + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'test-srv', + tokens: mcpTokens, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + findToken: tokenStore.findToken, + }); + + // Step 11: Verify new tokens work + const newResult = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }); + expect(newResult).not.toBeNull(); + expect(newResult!.access_token).toBe(newTokens.access_token); + + // Step 12: Verify new token works against server + const finalMcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${newResult!.access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + expect(finalMcpRes.status).toBe(200); + }); + }); + + describe('Concurrent token expiry with connection mutex', () => { + it('should handle multiple concurrent getTokens calls when token is expired', async () => { + const tokenStore = new InMemoryTokenStore(); + + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: 'enc:valid-refresh', + expiresIn: 86400, + }); + + let refreshCallCount = 0; + const refreshCallback = jest.fn().mockImplementation(async () => { + refreshCallCount++; + await new Promise((r) => setTimeout(r, 100)); + return { + access_token: `refreshed-token-${refreshCallCount}`, + token_type: 'Bearer', + expires_in: 3600, + obtained_at: Date.now(), + expires_at: Date.now() + 3600000, + }; + }); + + // Fire 3 concurrent getTokens calls via FlowStateManager (like the connection mutex does) + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const getTokensViaFlow = () => + flowManager.createFlowWithHandler('u1:test-srv', 'mcp_get_tokens', async () => { + return await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + }); + + const [r1, r2, r3] = await Promise.all([ + getTokensViaFlow(), + getTokensViaFlow(), + getTokensViaFlow(), + ]); + + // All should get tokens (either directly or via flow coalescing) + expect(r1).not.toBeNull(); + expect(r2).not.toBeNull(); + expect(r3).not.toBeNull(); + + // The refresh callback should only be called once due to flow coalescing + expect(refreshCallback).toHaveBeenCalledTimes(1); + }); + }); + + describe('Stale PENDING flow detection', () => { + it('should treat PENDING flows older than 2 minutes as stale', async () => { + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 300000, + ci: true, + }); + + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + authorizationUrl: 'https://example.com/auth', + }); + + // Manually age the flow to 3 minutes + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + if (state) { + state.createdAt = Date.now() - 3 * 60 * 1000; + await (flowStore as unknown as { set: (k: string, v: unknown) => Promise }).set( + `mcp_oauth:${flowId}`, + state, + ); + } + + const agedState = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(agedState?.status).toBe('PENDING'); + + const age = agedState?.createdAt ? Date.now() - agedState.createdAt : 0; + expect(age).toBeGreaterThan(2 * 60 * 1000); + + // A new flow should be created (the stale one would be deleted + recreated) + // This verifies our staleness check threshold + expect(age > PENDING_STALE_MS).toBe(true); + }); + + it('should not treat recent PENDING flows as stale', async () => { + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 300000, + ci: true, + }); + + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + authorizationUrl: 'https://example.com/auth', + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + const age = state?.createdAt ? Date.now() - state.createdAt : Infinity; + + expect(age < PENDING_STALE_MS).toBe(true); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthTokenStorage.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthTokenStorage.test.ts new file mode 100644 index 0000000000..3805586453 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthTokenStorage.test.ts @@ -0,0 +1,544 @@ +/** + * Integration tests for MCPTokenStorage.storeTokens() and MCPTokenStorage.getTokens(). + * + * Uses InMemoryTokenStore to exercise encrypt/decrypt round-trips, expiry calculation, + * refresh callback wiring, and ReauthenticationRequiredError paths. + */ + +import { MCPTokenStorage, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { InMemoryTokenStore } from './helpers/oauthTestServer'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +describe('MCPTokenStorage', () => { + let store: InMemoryTokenStore; + + beforeEach(() => { + store = new InMemoryTokenStore(); + jest.clearAllMocks(); + }); + + describe('storeTokens', () => { + it('should create new access token with expires_in', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved).not.toBeNull(); + expect(saved!.token).toBe('enc:at1'); + const expiresInMs = saved!.expiresAt.getTime() - Date.now(); + expect(expiresInMs).toBeGreaterThan(3500 * 1000); + expect(expiresInMs).toBeLessThanOrEqual(3600 * 1000); + }); + + it('should create new access token with expires_at (MCPOAuthTokens format)', async () => { + const expiresAt = Date.now() + 7200 * 1000; + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'at1', + token_type: 'Bearer', + expires_at: expiresAt, + obtained_at: Date.now(), + }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved).not.toBeNull(); + const diff = Math.abs(saved!.expiresAt.getTime() - expiresAt); + expect(diff).toBeLessThan(2000); + }); + + it('should default to 1-year expiry when none provided', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer' }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + const oneYearMs = 365 * 24 * 60 * 60 * 1000; + const expiresInMs = saved!.expiresAt.getTime() - Date.now(); + expect(expiresInMs).toBeGreaterThan(oneYearMs - 5000); + }); + + it('should update existing access token', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:old-token', + expiresIn: 3600, + }); + + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'new-token', token_type: 'Bearer', expires_in: 7200 }, + createToken: store.createToken, + updateToken: store.updateToken, + findToken: store.findToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved!.token).toBe('enc:new-token'); + }); + + it('should store refresh token alongside access token', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'at1', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'rt1', + }, + createToken: store.createToken, + }); + + const refreshSaved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + }); + expect(refreshSaved).not.toBeNull(); + expect(refreshSaved!.token).toBe('enc:rt1'); + }); + + it('should skip refresh token when not in response', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + }); + + const refreshSaved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + }); + expect(refreshSaved).toBeNull(); + }); + + it('should store client info when provided', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + clientInfo: { client_id: 'cid', client_secret: 'csec', redirect_uris: [] }, + }); + + const clientSaved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth_client', + identifier: 'mcp:srv1:client', + }); + expect(clientSaved).not.toBeNull(); + expect(clientSaved!.token).toContain('enc:'); + expect(clientSaved!.token).toContain('cid'); + }); + + it('should use existingTokens to skip DB lookups', async () => { + const findSpy = jest.fn(); + + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + updateToken: store.updateToken, + findToken: findSpy, + existingTokens: { + accessToken: null, + refreshToken: null, + clientInfoToken: null, + }, + }); + + expect(findSpy).not.toHaveBeenCalled(); + }); + + it('should handle invalid NaN expiry date', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'at1', + token_type: 'Bearer', + expires_at: NaN, + obtained_at: Date.now(), + }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved).not.toBeNull(); + const oneYearMs = 365 * 24 * 60 * 60 * 1000; + const expiresInMs = saved!.expiresAt.getTime() - Date.now(); + expect(expiresInMs).toBeGreaterThan(oneYearMs - 5000); + }); + }); + + describe('getTokens', () => { + it('should return valid non-expired tokens', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:valid-token', + expiresIn: 3600, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result).not.toBeNull(); + expect(result!.access_token).toBe('valid-token'); + expect(result!.token_type).toBe('Bearer'); + }); + + it('should return tokens with refresh token when available', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:at', + expiresIn: 3600, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result!.refresh_token).toBe('rt'); + }); + + it('should return tokens without refresh token field when none stored', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:at', + expiresIn: 3600, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result!.refresh_token).toBeUndefined(); + }); + + it('should throw ReauthenticationRequiredError when expired and no refresh', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should throw ReauthenticationRequiredError when missing and no refresh', async () => { + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should refresh expired access token when refresh token and callback are available', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockResolvedValue({ + access_token: 'refreshed-at', + token_type: 'Bearer', + expires_in: 3600, + obtained_at: Date.now(), + expires_at: Date.now() + 3600000, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + updateToken: store.updateToken, + refreshTokens, + }); + + expect(result).not.toBeNull(); + expect(result!.access_token).toBe('refreshed-at'); + expect(refreshTokens).toHaveBeenCalledWith( + 'rt', + expect.objectContaining({ userId: 'u1', serverName: 'srv1' }), + ); + }); + + it('should return null when refresh fails', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockRejectedValue(new Error('refresh failed')); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + updateToken: store.updateToken, + refreshTokens, + }); + + expect(result).toBeNull(); + }); + + it('should return null when no refreshTokens callback provided', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result).toBeNull(); + }); + + it('should return null when no createToken callback provided', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + refreshTokens: jest.fn(), + }); + + expect(result).toBeNull(); + }); + + it('should pass client info to refreshTokens metadata', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_client', + identifier: 'mcp:srv1:client', + token: 'enc:{"client_id":"cid","client_secret":"csec"}', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockResolvedValue({ + access_token: 'new-at', + token_type: 'Bearer', + expires_in: 3600, + }); + + await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + updateToken: store.updateToken, + refreshTokens, + }); + + expect(refreshTokens).toHaveBeenCalledWith( + 'rt', + expect.objectContaining({ + clientInfo: expect.objectContaining({ client_id: 'cid' }), + }), + ); + }); + + it('should handle unauthorized_client refresh error', async () => { + const { logger } = await import('@librechat/data-schemas'); + + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockRejectedValue(new Error('unauthorized_client')); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + refreshTokens, + }); + + expect(result).toBeNull(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('does not support refresh tokens'), + ); + }); + }); + + describe('storeTokens + getTokens round-trip', () => { + it('should store and retrieve tokens with full encrypt/decrypt cycle', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'my-access-token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'my-refresh-token', + }, + createToken: store.createToken, + clientInfo: { client_id: 'cid', client_secret: 'sec', redirect_uris: [] }, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result!.access_token).toBe('my-access-token'); + expect(result!.refresh_token).toBe('my-refresh-token'); + expect(result!.token_type).toBe('Bearer'); + expect(result!.obtained_at).toBeDefined(); + expect(result!.expires_at).toBeDefined(); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/helpers/oauthTestServer.ts b/packages/api/src/mcp/__tests__/helpers/oauthTestServer.ts new file mode 100644 index 0000000000..3b68b2ded4 --- /dev/null +++ b/packages/api/src/mcp/__tests__/helpers/oauthTestServer.ts @@ -0,0 +1,449 @@ +import * as http from 'http'; +import * as net from 'net'; +import { randomUUID, createHash } from 'crypto'; +import { z } from 'zod'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { FlowState } from '~/flow/types'; +import type { Socket } from 'net'; + +export class MockKeyv { + private store: Map>; + + constructor() { + this.store = new Map(); + } + + async get(key: string): Promise | undefined> { + return this.store.get(key); + } + + async set(key: string, value: FlowState, _ttl?: number): Promise { + this.store.set(key, value); + return true; + } + + async delete(key: string): Promise { + return this.store.delete(key); + } +} + +export function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +export function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +export interface OAuthTestServerOptions { + tokenTTLMs?: number; + issueRefreshTokens?: boolean; + refreshTokenTTLMs?: number; + rotateRefreshTokens?: boolean; +} + +export interface OAuthTestServer { + url: string; + port: number; + close: () => Promise; + issuedTokens: Set; + tokenTTL: number; + tokenIssueTimes: Map; + issuedRefreshTokens: Map; + registeredClients: Map; + getAuthCode: () => Promise; +} + +async function readRequestBody(req: http.IncomingMessage): Promise { + const chunks: Uint8Array[] = []; + for await (const chunk of req) { + chunks.push(chunk as Uint8Array); + } + return Buffer.concat(chunks).toString(); +} + +function parseTokenRequest(body: string, contentType: string | undefined): URLSearchParams | null { + if (contentType?.includes('application/x-www-form-urlencoded')) { + return new URLSearchParams(body); + } + if (contentType?.includes('application/json')) { + const json = JSON.parse(body) as Record; + return new URLSearchParams(json); + } + return new URLSearchParams(body); +} + +export async function createOAuthMCPServer( + options: OAuthTestServerOptions = {}, +): Promise { + const { + tokenTTLMs = 60000, + issueRefreshTokens = false, + refreshTokenTTLMs = 365 * 24 * 60 * 60 * 1000, + rotateRefreshTokens = false, + } = options; + + const sessions = new Map(); + const issuedTokens = new Set(); + const tokenIssueTimes = new Map(); + const issuedRefreshTokens = new Map(); + const refreshTokenIssueTimes = new Map(); + const authCodes = new Map(); + const registeredClients = new Map(); + + let port = 0; + + const httpServer = http.createServer(async (req, res) => { + const url = new URL(req.url ?? '/', `http://${req.headers.host}`); + + if (url.pathname === '/.well-known/oauth-authorization-server' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + issuer: `http://127.0.0.1:${port}`, + authorization_endpoint: `http://127.0.0.1:${port}/authorize`, + token_endpoint: `http://127.0.0.1:${port}/token`, + registration_endpoint: `http://127.0.0.1:${port}/register`, + response_types_supported: ['code'], + grant_types_supported: issueRefreshTokens + ? ['authorization_code', 'refresh_token'] + : ['authorization_code'], + token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], + code_challenge_methods_supported: ['S256'], + }), + ); + return; + } + + if (url.pathname === '/register' && req.method === 'POST') { + const body = await readRequestBody(req); + const data = JSON.parse(body) as { redirect_uris?: string[] }; + const clientId = `client-${randomUUID().slice(0, 8)}`; + const clientSecret = `secret-${randomUUID()}`; + registeredClients.set(clientId, { client_id: clientId, client_secret: clientSecret }); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + client_id: clientId, + client_secret: clientSecret, + redirect_uris: data.redirect_uris ?? [], + }), + ); + return; + } + + if (url.pathname === '/authorize') { + const code = randomUUID(); + const codeChallenge = url.searchParams.get('code_challenge') ?? undefined; + const codeChallengeMethod = url.searchParams.get('code_challenge_method') ?? undefined; + authCodes.set(code, { codeChallenge, codeChallengeMethod }); + const redirectUri = url.searchParams.get('redirect_uri') ?? ''; + const state = url.searchParams.get('state') ?? ''; + res.writeHead(302, { + Location: `${redirectUri}?code=${code}&state=${state}`, + }); + res.end(); + return; + } + + if (url.pathname === '/token' && req.method === 'POST') { + const body = await readRequestBody(req); + const params = parseTokenRequest(body, req.headers['content-type']); + if (!params) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_request' })); + return; + } + + const grantType = params.get('grant_type'); + + if (grantType === 'authorization_code') { + const code = params.get('code'); + const codeData = code ? authCodes.get(code) : undefined; + if (!code || !codeData) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + + if (codeData.codeChallenge) { + const codeVerifier = params.get('code_verifier'); + if (!codeVerifier) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + if (codeData.codeChallengeMethod === 'S256') { + const expected = createHash('sha256').update(codeVerifier).digest('base64url'); + if (expected !== codeData.codeChallenge) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + } + } + + authCodes.delete(code); + + const accessToken = randomUUID(); + issuedTokens.add(accessToken); + tokenIssueTimes.set(accessToken, Date.now()); + + const tokenResponse: Record = { + access_token: accessToken, + token_type: 'Bearer', + expires_in: Math.ceil(tokenTTLMs / 1000), + }; + + if (issueRefreshTokens) { + const refreshToken = randomUUID(); + issuedRefreshTokens.set(refreshToken, accessToken); + refreshTokenIssueTimes.set(refreshToken, Date.now()); + tokenResponse.refresh_token = refreshToken; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(tokenResponse)); + return; + } + + if (grantType === 'refresh_token' && issueRefreshTokens) { + const refreshToken = params.get('refresh_token'); + if (!refreshToken || !issuedRefreshTokens.has(refreshToken)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + + const issueTime = refreshTokenIssueTimes.get(refreshToken) ?? 0; + if (Date.now() - issueTime > refreshTokenTTLMs) { + issuedRefreshTokens.delete(refreshToken); + refreshTokenIssueTimes.delete(refreshToken); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + + const newAccessToken = randomUUID(); + issuedTokens.add(newAccessToken); + tokenIssueTimes.set(newAccessToken, Date.now()); + + const tokenResponse: Record = { + access_token: newAccessToken, + token_type: 'Bearer', + expires_in: Math.ceil(tokenTTLMs / 1000), + }; + + if (rotateRefreshTokens) { + issuedRefreshTokens.delete(refreshToken); + refreshTokenIssueTimes.delete(refreshToken); + const newRefreshToken = randomUUID(); + issuedRefreshTokens.set(newRefreshToken, newAccessToken); + refreshTokenIssueTimes.set(newRefreshToken, Date.now()); + tokenResponse.refresh_token = newRefreshToken; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(tokenResponse)); + return; + } + + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'unsupported_grant_type' })); + return; + } + + // All other paths require Bearer token auth + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_token' })); + return; + } + + const token = authHeader.slice(7); + if (!issuedTokens.has(token)) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_token' })); + return; + } + + const issueTime = tokenIssueTimes.get(token) ?? 0; + if (Date.now() - issueTime > tokenTTLMs) { + issuedTokens.delete(token); + tokenIssueTimes.delete(token); + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_token' })); + return; + } + + // Authenticated MCP request — route to transport + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + }); + const mcp = new McpServer({ name: 'oauth-test-server', version: '0.0.1' }); + mcp.tool('echo', { message: z.string() }, async (args) => ({ + content: [{ type: 'text' as const, text: `echo: ${args.message}` }], + })); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + port, + issuedTokens, + tokenTTL: tokenTTLMs, + tokenIssueTimes, + issuedRefreshTokens, + registeredClients, + getAuthCode: async () => { + const authRes = await fetch( + `http://127.0.0.1:${port}/authorize?redirect_uri=http://localhost&state=test`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + return new URL(location).searchParams.get('code') ?? ''; + }, + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +export interface InMemoryToken { + userId: string; + type: string; + identifier: string; + token: string; + expiresAt: Date; + createdAt: Date; + metadata?: Map | Record; +} + +export class InMemoryTokenStore { + private tokens: Map = new Map(); + + private key(filter: { userId?: string; type?: string; identifier?: string }): string { + return `${filter.userId}:${filter.type}:${filter.identifier}`; + } + + findToken = async (filter: { + userId?: string; + type?: string; + identifier?: string; + }): Promise => { + for (const token of this.tokens.values()) { + const matchUserId = !filter.userId || token.userId === filter.userId; + const matchType = !filter.type || token.type === filter.type; + const matchIdentifier = !filter.identifier || token.identifier === filter.identifier; + if (matchUserId && matchType && matchIdentifier) { + return token; + } + } + return null; + }; + + createToken = async (data: { + userId: string; + type: string; + identifier: string; + token: string; + expiresIn?: number; + metadata?: Record; + }): Promise => { + const expiresIn = data.expiresIn ?? 365 * 24 * 60 * 60; + const token: InMemoryToken = { + userId: data.userId, + type: data.type, + identifier: data.identifier, + token: data.token, + expiresAt: new Date(Date.now() + expiresIn * 1000), + createdAt: new Date(), + metadata: data.metadata, + }; + this.tokens.set(this.key(data), token); + return token; + }; + + updateToken = async ( + filter: { userId?: string; type?: string; identifier?: string }, + data: { + userId?: string; + type?: string; + identifier?: string; + token?: string; + expiresIn?: number; + metadata?: Record; + }, + ): Promise => { + const existing = await this.findToken(filter); + if (!existing) { + throw new Error(`Token not found for filter: ${JSON.stringify(filter)}`); + } + const existingKey = this.key(existing); + const expiresIn = + data.expiresIn ?? Math.floor((existing.expiresAt.getTime() - Date.now()) / 1000); + const updated: InMemoryToken = { + ...existing, + token: data.token ?? existing.token, + expiresAt: data.expiresIn ? new Date(Date.now() + expiresIn * 1000) : existing.expiresAt, + metadata: data.metadata ?? existing.metadata, + }; + this.tokens.set(existingKey, updated); + return updated; + }; + + deleteToken = async (filter: { + userId: string; + type: string; + identifier: string; + }): Promise => { + this.tokens.delete(this.key(filter)); + }; + + getAll(): InMemoryToken[] { + return [...this.tokens.values()]; + } + + clear(): void { + this.tokens.clear(); + } +} diff --git a/packages/api/src/mcp/__tests__/reconnection-storm.test.ts b/packages/api/src/mcp/__tests__/reconnection-storm.test.ts index c1cf0ec5df..e073dca8a3 100644 --- a/packages/api/src/mcp/__tests__/reconnection-storm.test.ts +++ b/packages/api/src/mcp/__tests__/reconnection-storm.test.ts @@ -12,8 +12,12 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import type { Socket } from 'net'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; import { OAuthReconnectionTracker } from '~/mcp/oauth/OAuthReconnectionTracker'; +import { createOAuthMCPServer } from './helpers/oauthTestServer'; import { MCPConnection } from '~/mcp/connection'; +import { mcpConfig } from '~/mcp/mcpConfig'; jest.mock('@librechat/data-schemas', () => ({ logger: { @@ -143,16 +147,17 @@ afterEach(() => { /* ------------------------------------------------------------------ */ /* Fix #2 — Circuit breaker trips after rapid connect/disconnect */ -/* cycles (5 cycles within 60s -> 30s cooldown) */ +/* cycles (CB_MAX_CYCLES within window -> cooldown) */ /* ------------------------------------------------------------------ */ describe('Fix #2: Circuit breaker stops rapid reconnect cycling', () => { - it('blocks connection after 5 rapid cycles via static circuit breaker', async () => { + it('blocks connection after CB_MAX_CYCLES rapid cycles via static circuit breaker', async () => { const srv = await startMCPServer(); const conn = createConnection('cycling-server', srv.url); let completedCycles = 0; let breakerMessage = ''; - for (let cycle = 0; cycle < 10; cycle++) { + const maxAttempts = mcpConfig.CB_MAX_CYCLES * 2; + for (let cycle = 0; cycle < maxAttempts; cycle++) { try { await conn.connect(); await teardownConnection(conn); @@ -166,7 +171,7 @@ describe('Fix #2: Circuit breaker stops rapid reconnect cycling', () => { } expect(breakerMessage).toContain('Circuit breaker is open'); - expect(completedCycles).toBeLessThanOrEqual(5); + expect(completedCycles).toBeLessThanOrEqual(mcpConfig.CB_MAX_CYCLES); await srv.close(); }); @@ -266,12 +271,13 @@ describe('Fix #4: Circuit breaker state persists across instance replacement', ( /* recordFailedRound() in the catch path */ /* ------------------------------------------------------------------ */ describe('Fix #5: Dead server triggers circuit breaker', () => { - it('3 failures trigger backoff, blocking subsequent attempts before they reach the SDK', async () => { + it('failures trigger backoff, blocking subsequent attempts before they reach the SDK', async () => { const conn = createConnection('dead', 'http://127.0.0.1:1/mcp', 1000); const spy = jest.spyOn(conn.client, 'connect'); + const totalAttempts = mcpConfig.CB_MAX_FAILED_ROUNDS + 2; const errors: string[] = []; - for (let i = 0; i < 5; i++) { + for (let i = 0; i < totalAttempts; i++) { try { await conn.connect(); } catch (e) { @@ -279,8 +285,8 @@ describe('Fix #5: Dead server triggers circuit breaker', () => { } } - expect(spy.mock.calls.length).toBe(3); - expect(errors).toHaveLength(5); + expect(spy.mock.calls.length).toBe(mcpConfig.CB_MAX_FAILED_ROUNDS); + expect(errors).toHaveLength(totalAttempts); expect(errors.filter((m) => m.includes('Circuit breaker is open'))).toHaveLength(2); await conn.disconnect(); @@ -295,7 +301,7 @@ describe('Fix #5: Dead server triggers circuit breaker', () => { userId: 'user-A', }); - for (let i = 0; i < 3; i++) { + for (let i = 0; i < mcpConfig.CB_MAX_FAILED_ROUNDS; i++) { try { await userA.connect(); } catch { @@ -332,7 +338,7 @@ describe('Fix #5: Dead server triggers circuit breaker', () => { serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, userId: 'user-A', }); - for (let i = 0; i < 3; i++) { + for (let i = 0; i < mcpConfig.CB_MAX_FAILED_ROUNDS; i++) { try { await userA.connect(); } catch { @@ -448,13 +454,14 @@ describe('Fix #6: OAuth failure uses cooldown-based retry', () => { /* Integration: Circuit breaker caps rapid cycling with real transport */ /* ------------------------------------------------------------------ */ describe('Cascade: Circuit breaker caps rapid cycling', () => { - it('breaker trips before 10 cycles complete against a live server', async () => { + it('breaker trips before double CB_MAX_CYCLES complete against a live server', async () => { const srv = await startMCPServer(); const conn = createConnection('cascade', srv.url); const spy = jest.spyOn(conn.client, 'connect'); let completedCycles = 0; - for (let i = 0; i < 10; i++) { + const maxAttempts = mcpConfig.CB_MAX_CYCLES * 2; + for (let i = 0; i < maxAttempts; i++) { try { await conn.connect(); await teardownConnection(conn); @@ -469,8 +476,8 @@ describe('Cascade: Circuit breaker caps rapid cycling', () => { } } - expect(completedCycles).toBeLessThanOrEqual(5); - expect(spy.mock.calls.length).toBeLessThanOrEqual(5); + expect(completedCycles).toBeLessThanOrEqual(mcpConfig.CB_MAX_CYCLES); + expect(spy.mock.calls.length).toBeLessThanOrEqual(mcpConfig.CB_MAX_CYCLES); await srv.close(); }); @@ -501,6 +508,146 @@ describe('Cascade: Circuit breaker caps rapid cycling', () => { }, 30_000); }); +/* ------------------------------------------------------------------ */ +/* OAuth: cycle recovery after successful OAuth reconnect */ +/* ------------------------------------------------------------------ */ +describe('OAuth: cycle budget recovery after successful OAuth', () => { + let oauthServer: OAuthTestServer; + + beforeEach(async () => { + oauthServer = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + afterEach(async () => { + await oauthServer.close(); + }); + + async function exchangeCodeForToken(serverUrl: string): Promise { + const authRes = await fetch(`${serverUrl}authorize?redirect_uri=http://localhost&state=test`, { + redirect: 'manual', + }); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + const tokenRes = await fetch(`${serverUrl}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const data = (await tokenRes.json()) as { access_token: string }; + return data.access_token; + } + + it('should decrement cycle count after successful OAuth recovery', async () => { + const serverName = 'oauth-cycle-test'; + MCPConnection.clearCooldown(serverName); + + const conn = new MCPConnection({ + serverName, + serverConfig: { type: 'streamable-http', url: oauthServer.url, initTimeout: 10000 }, + userId: 'user-1', + }); + + // When oauthRequired fires, get a token and emit oauthHandled + // This triggers the oauthRecovery path inside connectClient + conn.on('oauthRequired', async () => { + const accessToken = await exchangeCodeForToken(oauthServer.url); + conn.setOAuthTokens({ + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + conn.emit('oauthHandled'); + }); + + // connect() → 401 → oauthRequired → oauthHandled → connectClient returns + // connect() sees not connected → throws "Connection not established" + await expect(conn.connect()).rejects.toThrow('Connection not established'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (MCPConnection as any).circuitBreakers.get(serverName); + const cyclesBeforeRetry = cb.cycleCount; + + // Retry — should succeed and decrement cycle count via oauthRecovery + await conn.connect(); + expect(await conn.isConnected()).toBe(true); + + const cyclesAfterSuccess = cb.cycleCount; + // The retry adds +1 cycle (disconnect(false)) then -1 (oauthRecovery decrement) + // So cyclesAfterSuccess should equal cyclesBeforeRetry, not cyclesBeforeRetry + 1 + expect(cyclesAfterSuccess).toBe(cyclesBeforeRetry); + + await teardownConnection(conn); + }); + + it('should allow more OAuth reconnects than non-OAuth before breaker trips', async () => { + const serverName = 'oauth-budget'; + MCPConnection.clearCooldown(serverName); + + // Each OAuth flow: connect (+1) → 401 → oauthHandled → retry connect (+1) → success (-1) = net 1 + // Without the decrement it would be net 2 per flow, tripping the breaker after ~2 users + let successfulFlows = 0; + for (let i = 0; i < 10; i++) { + const conn = new MCPConnection({ + serverName, + serverConfig: { type: 'streamable-http', url: oauthServer.url, initTimeout: 10000 }, + userId: `user-${i}`, + }); + + conn.on('oauthRequired', async () => { + const accessToken = await exchangeCodeForToken(oauthServer.url); + conn.setOAuthTokens({ + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + conn.emit('oauthHandled'); + }); + + try { + // First connect: 401 → oauthHandled → returns without connection + await conn.connect().catch(() => {}); + // Retry: succeeds with token, decrements cycle + await conn.connect(); + successfulFlows++; + await teardownConnection(conn); + } catch (e) { + conn.removeAllListeners(); + if ((e as Error).message.includes('Circuit breaker is open')) { + break; + } + } + } + + // With the oauthRecovery decrement, each flow is net ~1 cycle instead of ~2, + // so we should get more successful flows before the breaker trips + expect(successfulFlows).toBeGreaterThanOrEqual(3); + }); + + it('should not decrement cycle count when OAuth fails', async () => { + const serverName = 'oauth-failed-no-decrement'; + MCPConnection.clearCooldown(serverName); + + const conn = new MCPConnection({ + serverName, + serverConfig: { type: 'streamable-http', url: oauthServer.url, initTimeout: 10000 }, + userId: 'user-1', + }); + + conn.on('oauthRequired', () => { + conn.emit('oauthFailed', new Error('user denied')); + }); + + await expect(conn.connect()).rejects.toThrow(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (MCPConnection as any).circuitBreakers.get(serverName); + const cyclesAfterFailure = cb.cycleCount; + + // connect() recorded +1 cycle, oauthFailed should NOT decrement + expect(cyclesAfterFailure).toBeGreaterThanOrEqual(1); + + conn.removeAllListeners(); + }); +}); + /* ------------------------------------------------------------------ */ /* Sanity: Real transport works end-to-end */ /* ------------------------------------------------------------------ */ diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index cac0a4afc5..8dc857cd3b 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -82,14 +82,6 @@ interface CircuitBreakerState { failedBackoffUntil: number; } -const CB_MAX_CYCLES = 5; -const CB_CYCLE_WINDOW_MS = 60_000; -const CB_CYCLE_COOLDOWN_MS = 30_000; - -const CB_MAX_FAILED_ROUNDS = 3; -const CB_FAILED_WINDOW_MS = 120_000; -const CB_BASE_BACKOFF_MS = 30_000; -const CB_MAX_BACKOFF_MS = 300_000; /** Default body timeout for Streamable HTTP GET SSE streams that idle between server pushes */ const DEFAULT_SSE_READ_TIMEOUT = FIVE_MINUTES; @@ -281,6 +273,7 @@ export class MCPConnection extends EventEmitter { private oauthTokens?: MCPOAuthTokens | null; private requestHeaders?: Record | null; private oauthRequired = false; + private oauthRecovery = false; private readonly useSSRFProtection: boolean; iconPath?: string; timeout?: number; @@ -325,17 +318,17 @@ export class MCPConnection extends EventEmitter { private recordCycle(): void { const cb = this.getCircuitBreaker(); const now = Date.now(); - if (now - cb.cycleWindowStart > CB_CYCLE_WINDOW_MS) { + if (now - cb.cycleWindowStart > mcpConfig.CB_CYCLE_WINDOW_MS) { cb.cycleCount = 0; cb.cycleWindowStart = now; } cb.cycleCount++; - if (cb.cycleCount >= CB_MAX_CYCLES) { - cb.cooldownUntil = now + CB_CYCLE_COOLDOWN_MS; + if (cb.cycleCount >= mcpConfig.CB_MAX_CYCLES) { + cb.cooldownUntil = now + mcpConfig.CB_CYCLE_COOLDOWN_MS; cb.cycleCount = 0; cb.cycleWindowStart = now; logger.warn( - `${this.getLogPrefix()} Circuit breaker: too many cycles, cooling down for ${CB_CYCLE_COOLDOWN_MS}ms`, + `${this.getLogPrefix()} Circuit breaker: too many cycles, cooling down for ${mcpConfig.CB_CYCLE_COOLDOWN_MS}ms`, ); } } @@ -343,15 +336,16 @@ export class MCPConnection extends EventEmitter { private recordFailedRound(): void { const cb = this.getCircuitBreaker(); const now = Date.now(); - if (now - cb.failedWindowStart > CB_FAILED_WINDOW_MS) { + if (now - cb.failedWindowStart > mcpConfig.CB_FAILED_WINDOW_MS) { cb.failedRounds = 0; cb.failedWindowStart = now; } cb.failedRounds++; - if (cb.failedRounds >= CB_MAX_FAILED_ROUNDS) { + if (cb.failedRounds >= mcpConfig.CB_MAX_FAILED_ROUNDS) { const backoff = Math.min( - CB_BASE_BACKOFF_MS * Math.pow(2, cb.failedRounds - CB_MAX_FAILED_ROUNDS), - CB_MAX_BACKOFF_MS, + mcpConfig.CB_BASE_BACKOFF_MS * + Math.pow(2, cb.failedRounds - mcpConfig.CB_MAX_FAILED_ROUNDS), + mcpConfig.CB_MAX_BACKOFF_MS, ); cb.failedBackoffUntil = now + backoff; logger.warn( @@ -367,6 +361,13 @@ export class MCPConnection extends EventEmitter { cb.failedBackoffUntil = 0; } + public static decrementCycleCount(serverName: string): void { + const cb = MCPConnection.circuitBreakers.get(serverName); + if (cb && cb.cycleCount > 0) { + cb.cycleCount--; + } + } + setRequestHeaders(headers: Record | null): void { if (!headers) { return; @@ -816,6 +817,13 @@ export class MCPConnection extends EventEmitter { this.emit('connectionChange', 'connected'); this.reconnectAttempts = 0; this.resetFailedRounds(); + if (this.oauthRecovery) { + MCPConnection.decrementCycleCount(this.serverName); + this.oauthRecovery = false; + logger.debug( + `${this.getLogPrefix()} OAuth recovery: decremented cycle count after successful reconnect`, + ); + } } catch (error) { // Check if it's a rate limit error - stop immediately to avoid making it worse if (this.isRateLimitError(error)) { @@ -899,9 +907,8 @@ export class MCPConnection extends EventEmitter { try { // Wait for OAuth to be handled await oauthHandledPromise; - // Reset the oauthRequired flag this.oauthRequired = false; - // Don't throw the error - just return so connection can be retried + this.oauthRecovery = true; logger.info( `${this.getLogPrefix()} OAuth handled successfully, connection will be retried`, ); diff --git a/packages/api/src/mcp/mcpConfig.ts b/packages/api/src/mcp/mcpConfig.ts index f3efd3592b..a81752e909 100644 --- a/packages/api/src/mcp/mcpConfig.ts +++ b/packages/api/src/mcp/mcpConfig.ts @@ -12,4 +12,18 @@ export const mcpConfig = { USER_CONNECTION_IDLE_TIMEOUT: math( process.env.MCP_USER_CONNECTION_IDLE_TIMEOUT ?? 15 * 60 * 1000, ), + /** Max connect/disconnect cycles before the circuit breaker trips. Default: 7 */ + CB_MAX_CYCLES: math(process.env.MCP_CB_MAX_CYCLES ?? 7), + /** Sliding window (ms) for counting cycles. Default: 45s */ + CB_CYCLE_WINDOW_MS: math(process.env.MCP_CB_CYCLE_WINDOW_MS ?? 45_000), + /** Cooldown (ms) after the cycle breaker trips. Default: 15s */ + CB_CYCLE_COOLDOWN_MS: math(process.env.MCP_CB_CYCLE_COOLDOWN_MS ?? 15_000), + /** Max consecutive failed connection rounds before backoff. Default: 3 */ + CB_MAX_FAILED_ROUNDS: math(process.env.MCP_CB_MAX_FAILED_ROUNDS ?? 3), + /** Sliding window (ms) for counting failed rounds. Default: 120s */ + CB_FAILED_WINDOW_MS: math(process.env.MCP_CB_FAILED_WINDOW_MS ?? 120_000), + /** Base backoff (ms) after failed round threshold is reached. Default: 30s */ + CB_BASE_BACKOFF_MS: math(process.env.MCP_CB_BASE_BACKOFF_MS ?? 30_000), + /** Max backoff cap (ms) for exponential backoff. Default: 300s */ + CB_MAX_BACKOFF_MS: math(process.env.MCP_CB_MAX_BACKOFF_MS ?? 300_000), }; diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 83e855591e..366d0d2fde 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -426,8 +426,8 @@ export class MCPOAuthHandler { scope: config.scope, }); - /** Add state parameter with flowId to the authorization URL */ - authorizationUrl.searchParams.set('state', flowId); + /** Add cryptographic state parameter to the authorization URL */ + authorizationUrl.searchParams.set('state', state); logger.debug(`[MCPOAuth] Added state parameter to authorization URL`); const flowMetadata: MCPOAuthFlowMetadata = { @@ -505,8 +505,8 @@ export class MCPOAuthHandler { `[MCPOAuth] Authorization URL: ${sanitizeUrlForLogging(authorizationUrl.toString())}`, ); - /** Add state parameter with flowId to the authorization URL */ - authorizationUrl.searchParams.set('state', flowId); + /** Add cryptographic state parameter to the authorization URL */ + authorizationUrl.searchParams.set('state', state); logger.debug(`[MCPOAuth] Added state parameter to authorization URL`); if (resourceMetadata?.resource != null && resourceMetadata.resource) { @@ -672,6 +672,44 @@ export class MCPOAuthHandler { return randomBytes(32).toString('base64url'); } + private static readonly STATE_MAP_TYPE = 'mcp_oauth_state'; + + /** + * Stores a mapping from the opaque OAuth state parameter to the flowId. + * This allows the callback to resolve the flowId from an unguessable state + * value, preventing attackers from forging callback requests. + */ + static async storeStateMapping( + state: string, + flowId: string, + flowManager: FlowStateManager, + ): Promise { + await flowManager.initFlow(state, this.STATE_MAP_TYPE, { flowId }); + } + + /** + * Resolves an opaque OAuth state parameter back to the original flowId. + * Returns null if the state is not found (expired or never stored). + */ + static async resolveStateToFlowId( + state: string, + flowManager: FlowStateManager, + ): Promise { + const mapping = await flowManager.getFlowState(state, this.STATE_MAP_TYPE); + return (mapping?.metadata?.flowId as string) ?? null; + } + + /** + * Deletes an orphaned state mapping when a flow is replaced. + * Prevents old authorization URLs from resolving after a flow restart. + */ + static async deleteStateMapping( + state: string, + flowManager: FlowStateManager, + ): Promise { + await flowManager.deleteFlow(state, this.STATE_MAP_TYPE); + } + /** * Gets the default redirect URI for a server */ diff --git a/packages/api/src/mcp/oauth/tokens.ts b/packages/api/src/mcp/oauth/tokens.ts index 005ed7dd9a..7b1d189347 100644 --- a/packages/api/src/mcp/oauth/tokens.ts +++ b/packages/api/src/mcp/oauth/tokens.ts @@ -4,6 +4,15 @@ import type { TokenMethods, IToken } from '@librechat/data-schemas'; import type { MCPOAuthTokens, ExtendedOAuthTokens, OAuthMetadata } from './types'; import { isSystemUserId } from '~/mcp/enum'; +export class ReauthenticationRequiredError extends Error { + constructor(serverName: string, reason: 'expired' | 'missing') { + super( + `Re-authentication required for "${serverName}": access token ${reason} and no refresh token available`, + ); + this.name = 'ReauthenticationRequiredError'; + } +} + interface StoreTokensParams { userId: string; serverName: string; @@ -27,7 +36,12 @@ interface GetTokensParams { findToken: TokenMethods['findToken']; refreshTokens?: ( refreshToken: string, - metadata: { userId: string; serverName: string; identifier: string }, + metadata: { + userId: string; + serverName: string; + identifier: string; + clientInfo?: OAuthClientInformation; + }, ) => Promise; createToken?: TokenMethods['createToken']; updateToken?: TokenMethods['updateToken']; @@ -273,10 +287,11 @@ export class MCPTokenStorage { }); if (!refreshTokenData) { + const reason = isMissing ? 'missing' : 'expired'; logger.info( - `${logPrefix} Access token ${isMissing ? 'missing' : 'expired'} and no refresh token available`, + `${logPrefix} Access token ${reason} and no refresh token available — re-authentication required`, ); - return null; + throw new ReauthenticationRequiredError(serverName, reason); } if (!refreshTokens) { @@ -395,6 +410,9 @@ export class MCPTokenStorage { logger.debug(`${logPrefix} Loaded existing OAuth tokens from storage`); return tokens; } catch (error) { + if (error instanceof ReauthenticationRequiredError) { + throw error; + } logger.error(`${logPrefix} Failed to retrieve tokens`, error); return null; } diff --git a/packages/api/src/mcp/oauth/types.ts b/packages/api/src/mcp/oauth/types.ts index 178e20e35b..2138b4a782 100644 --- a/packages/api/src/mcp/oauth/types.ts +++ b/packages/api/src/mcp/oauth/types.ts @@ -88,6 +88,7 @@ export interface MCPOAuthFlowMetadata extends FlowMetadata { clientInfo?: OAuthClientInformation; metadata?: OAuthMetadata; resourceMetadata?: OAuthProtectedResourceMetadata; + authorizationUrl?: string; } export interface MCPOAuthTokens extends OAuthTokens { From 9a5d7eaa4ef1dbae21d06ea82e587c54e867b48b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Mar 2026 23:14:52 -0400 Subject: [PATCH 019/111] =?UTF-8?q?=E2=9A=A1=20refactor:=20Replace=20`tikt?= =?UTF-8?q?oken`=20with=20`ai-tokenizer`=20(#12175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Update dependencies by adding ai-tokenizer and removing tiktoken - Added ai-tokenizer version 1.0.6 to package.json and package-lock.json across multiple packages. - Removed tiktoken version 1.0.15 from package.json and package-lock.json in the same locations, streamlining dependency management. * refactor: replace js-tiktoken with ai-tokenizer - Added support for 'claude' encoding in the AgentClient class to improve model compatibility. - Updated Tokenizer class to utilize 'ai-tokenizer' for both 'o200k_base' and 'claude' encodings, replacing the previous 'tiktoken' dependency. - Refactored tests to reflect changes in tokenizer behavior and ensure accurate token counting for both encoding types. - Removed deprecated references to 'tiktoken' and adjusted related tests for improved clarity and functionality. * chore: remove tiktoken mocks from DALLE3 tests - Eliminated mock implementations of 'tiktoken' from DALLE3-related test files to streamline test setup and align with recent dependency updates. - Adjusted related test structures to ensure compatibility with the new tokenizer implementation. * chore: Add distinct encoding support for Anthropic Claude models - Introduced a new method `getEncoding` in the AgentClient class to handle the specific BPE tokenizer for Claude models, ensuring compatibility with the distinct encoding requirements. - Updated documentation to clarify the encoding logic for Claude and other models. * docs: Update return type documentation for getEncoding method in AgentClient - Clarified the return type of the getEncoding method to specify that it can return an EncodingName or undefined, enhancing code readability and type safety. * refactor: Tokenizer class and error handling - Exported the EncodingName type for broader usage. - Renamed encodingMap to encodingData for clarity. - Improved error handling in getTokenCount method to ensure recovery attempts are logged and return 0 on failure. - Updated countTokens function documentation to specify the use of 'o200k_base' encoding. * refactor: Simplify encoding documentation and export type - Updated the getEncoding method documentation to clarify the default behavior for non-Anthropic Claude models. - Exported the EncodingName type separately from the Tokenizer module for improved clarity and usage. * test: Update text processing tests for token limits - Adjusted test cases to handle smaller text sizes, changing scenarios from ~120k tokens to ~20k tokens for both the real tokenizer and countTokens functions. - Updated token limits in tests to reflect new constraints, ensuring tests accurately assess performance and call reduction. - Enhanced console log messages for clarity regarding token counts and reductions in the updated scenarios. * refactor: Update Tokenizer imports and exports - Moved Tokenizer and countTokens exports to the tokenizer module for better organization. - Adjusted imports in memory.ts to reflect the new structure, ensuring consistent usage across the codebase. - Updated memory.test.ts to mock the Tokenizer from the correct module path, enhancing test accuracy. * refactor: Tokenizer initialization and error handling - Introduced an async `initEncoding` method to preload tokenizers, improving performance and accuracy in token counting. - Updated `getTokenCount` to handle uninitialized tokenizers more gracefully, ensuring proper recovery and logging on errors. - Removed deprecated synchronous tokenizer retrieval, streamlining the overall tokenizer management process. * test: Enhance tokenizer tests with initialization and encoding checks - Added `beforeAll` hooks to initialize tokenizers for 'o200k_base' and 'claude' encodings before running tests, ensuring proper setup. - Updated tests to validate the loading of encodings and the correctness of token counts for both 'o200k_base' and 'claude'. - Improved test structure to deduplicate concurrent initialization calls, enhancing performance and reliability. --- .../structured/specs/DALLE3-proxy.spec.js | 1 - .../tools/structured/specs/DALLE3.spec.js | 9 -- api/package.json | 2 +- api/server/controllers/agents/client.js | 4 + api/strategies/samlStrategy.spec.js | 1 - package-lock.json | 23 ++- packages/api/package.json | 2 +- .../api/src/agents/__tests__/memory.test.ts | 5 +- packages/api/src/agents/memory.ts | 3 +- packages/api/src/index.ts | 2 + packages/api/src/utils/index.ts | 1 - packages/api/src/utils/text.spec.ts | 62 +++----- packages/api/src/utils/tokenizer.spec.ts | 137 ++++-------------- packages/api/src/utils/tokenizer.ts | 98 +++++-------- packages/api/src/utils/tokens.ts | 39 ----- 15 files changed, 112 insertions(+), 277 deletions(-) diff --git a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js index 4481a7d70f..262842b3c2 100644 --- a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js @@ -1,7 +1,6 @@ const DALLE3 = require('../DALLE3'); const { ProxyAgent } = require('undici'); -jest.mock('tiktoken'); const processFileURL = jest.fn(); describe('DALLE3 Proxy Configuration', () => { diff --git a/api/app/clients/tools/structured/specs/DALLE3.spec.js b/api/app/clients/tools/structured/specs/DALLE3.spec.js index d2040989f9..6071929bfc 100644 --- a/api/app/clients/tools/structured/specs/DALLE3.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3.spec.js @@ -14,15 +14,6 @@ jest.mock('@librechat/data-schemas', () => { }; }); -jest.mock('tiktoken', () => { - return { - encoding_for_model: jest.fn().mockReturnValue({ - encode: jest.fn(), - decode: jest.fn(), - }), - }; -}); - const processFileURL = jest.fn(); const generate = jest.fn(); diff --git a/api/package.json b/api/package.json index fcd353af57..1618481b58 100644 --- a/api/package.json +++ b/api/package.json @@ -51,6 +51,7 @@ "@modelcontextprotocol/sdk": "^1.27.1", "@node-saml/passport-saml": "^5.1.0", "@smithy/node-http-handler": "^4.4.5", + "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "bcryptjs": "^2.4.3", "compression": "^1.8.1", @@ -106,7 +107,6 @@ "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "sharp": "^0.33.5", - "tiktoken": "^1.0.15", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", "undici": "^7.18.2", diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 5f99a0762b..0ecd62b819 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1172,7 +1172,11 @@ class AgentClient extends BaseClient { } } + /** Anthropic Claude models use a distinct BPE tokenizer; all others default to o200k_base. */ getEncoding() { + if (this.model && this.model.toLowerCase().includes('claude')) { + return 'claude'; + } return 'o200k_base'; } diff --git a/api/strategies/samlStrategy.spec.js b/api/strategies/samlStrategy.spec.js index 06c969ce46..1d16719b87 100644 --- a/api/strategies/samlStrategy.spec.js +++ b/api/strategies/samlStrategy.spec.js @@ -1,5 +1,4 @@ // --- Mocks --- -jest.mock('tiktoken'); jest.mock('fs'); jest.mock('path'); jest.mock('node-fetch'); diff --git a/package-lock.json b/package-lock.json index 09c5219afb..a2db2df389 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "@modelcontextprotocol/sdk": "^1.27.1", "@node-saml/passport-saml": "^5.1.0", "@smithy/node-http-handler": "^4.4.5", + "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "bcryptjs": "^2.4.3", "compression": "^1.8.1", @@ -121,7 +122,6 @@ "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "sharp": "^0.33.5", - "tiktoken": "^1.0.15", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", "undici": "^7.18.2", @@ -22230,6 +22230,20 @@ "node": ">= 14" } }, + "node_modules/ai-tokenizer": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ai-tokenizer/-/ai-tokenizer-1.0.6.tgz", + "integrity": "sha512-GaakQFxen0pRH/HIA4v68ZM40llCH27HUYUSBLK+gVuZ57e53pYJe1xFvSTj4sJJjbWU92m1X6NjPWyeWkFDow==", + "license": "MIT", + "peerDependencies": { + "ai": "^5.0.0" + }, + "peerDependenciesMeta": { + "ai": { + "optional": true + } + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -41485,11 +41499,6 @@ "node": ">=0.8" } }, - "node_modules/tiktoken": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.15.tgz", - "integrity": "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw==" - }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -44200,6 +44209,7 @@ "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", + "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "connect-redis": "^8.1.0", "eventsource": "^3.0.2", @@ -44222,7 +44232,6 @@ "node-fetch": "2.7.0", "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", - "tiktoken": "^1.0.15", "undici": "^7.18.2", "zod": "^3.22.4" } diff --git a/packages/api/package.json b/packages/api/package.json index 46587797a5..966447c51b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -94,6 +94,7 @@ "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", + "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "connect-redis": "^8.1.0", "eventsource": "^3.0.2", @@ -116,7 +117,6 @@ "node-fetch": "2.7.0", "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", - "tiktoken": "^1.0.15", "undici": "^7.18.2", "zod": "^3.22.4" } diff --git a/packages/api/src/agents/__tests__/memory.test.ts b/packages/api/src/agents/__tests__/memory.test.ts index 74cd0f4354..dabe6de629 100644 --- a/packages/api/src/agents/__tests__/memory.test.ts +++ b/packages/api/src/agents/__tests__/memory.test.ts @@ -22,8 +22,9 @@ jest.mock('winston', () => ({ })); // Mock the Tokenizer -jest.mock('~/utils', () => ({ - Tokenizer: { +jest.mock('~/utils/tokenizer', () => ({ + __esModule: true, + default: { getTokenCount: jest.fn((text: string) => text.length), // Simple mock: 1 char = 1 token }, })); diff --git a/packages/api/src/agents/memory.ts b/packages/api/src/agents/memory.ts index b8f65a9772..b7ae8a8123 100644 --- a/packages/api/src/agents/memory.ts +++ b/packages/api/src/agents/memory.ts @@ -19,7 +19,8 @@ import type { TAttachment, MemoryArtifact } from 'librechat-data-provider'; import type { BaseMessage, ToolMessage } from '@langchain/core/messages'; import type { Response as ServerResponse } from 'express'; import { GenerationJobManager } from '~/stream/GenerationJobManager'; -import { Tokenizer, resolveHeaders, createSafeUser } from '~/utils'; +import { resolveHeaders, createSafeUser } from '~/utils'; +import Tokenizer from '~/utils/tokenizer'; type RequiredMemoryMethods = Pick< MemoryMethods, diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index a7edb3882d..687ee7aa49 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -15,6 +15,8 @@ export * from './mcp/errors'; /* Utilities */ export * from './mcp/utils'; export * from './utils'; +export { default as Tokenizer, countTokens } from './utils/tokenizer'; +export type { EncodingName } from './utils/tokenizer'; export * from './db/utils'; /* OAuth */ export * from './oauth'; diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 470780cd5c..441c2e02d7 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -19,7 +19,6 @@ export * from './promise'; export * from './sanitizeTitle'; export * from './tempChatRetention'; export * from './text'; -export { default as Tokenizer, countTokens } from './tokenizer'; export * from './yaml'; export * from './http'; export * from './tokens'; diff --git a/packages/api/src/utils/text.spec.ts b/packages/api/src/utils/text.spec.ts index 1b8d8aac98..30185f9da7 100644 --- a/packages/api/src/utils/text.spec.ts +++ b/packages/api/src/utils/text.spec.ts @@ -65,7 +65,7 @@ const createRealTokenCounter = () => { let callCount = 0; const tokenCountFn = (text: string): number => { callCount++; - return Tokenizer.getTokenCount(text, 'cl100k_base'); + return Tokenizer.getTokenCount(text, 'o200k_base'); }; return { tokenCountFn, @@ -590,9 +590,9 @@ describe('processTextWithTokenLimit', () => { }); }); - describe('direct comparison with REAL tiktoken tokenizer', () => { - beforeEach(() => { - Tokenizer.freeAndResetAllEncoders(); + describe('direct comparison with REAL ai-tokenizer', () => { + beforeAll(async () => { + await Tokenizer.initEncoding('o200k_base'); }); it('should produce valid truncation with real tokenizer', async () => { @@ -611,7 +611,7 @@ describe('processTextWithTokenLimit', () => { expect(result.text.length).toBeLessThan(text.length); }); - it('should use fewer tiktoken calls than old implementation (realistic text)', async () => { + it('should use fewer tokenizer calls than old implementation (realistic text)', async () => { const oldCounter = createRealTokenCounter(); const newCounter = createRealTokenCounter(); const text = createRealisticText(15000); @@ -623,8 +623,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -634,17 +632,17 @@ describe('processTextWithTokenLimit', () => { const oldCalls = oldCounter.getCallCount(); const newCalls = newCounter.getCallCount(); - console.log(`[Real tiktoken ~15k tokens] OLD: ${oldCalls} calls, NEW: ${newCalls} calls`); - console.log(`[Real tiktoken] Reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); + console.log(`[Real tokenizer ~15k tokens] OLD: ${oldCalls} calls, NEW: ${newCalls} calls`); + console.log(`[Real tokenizer] Reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); expect(newCalls).toBeLessThan(oldCalls); }); - it('should handle the reported user scenario with real tokenizer (~120k tokens)', async () => { + it('should handle large text with real tokenizer (~20k tokens)', async () => { const oldCounter = createRealTokenCounter(); const newCounter = createRealTokenCounter(); - const text = createRealisticText(120000); - const tokenLimit = 100000; + const text = createRealisticText(20000); + const tokenLimit = 15000; const startOld = performance.now(); await processTextWithTokenLimitOLD({ @@ -654,8 +652,6 @@ describe('processTextWithTokenLimit', () => { }); const timeOld = performance.now() - startOld; - Tokenizer.freeAndResetAllEncoders(); - const startNew = performance.now(); const result = await processTextWithTokenLimit({ text, @@ -667,9 +663,9 @@ describe('processTextWithTokenLimit', () => { const oldCalls = oldCounter.getCallCount(); const newCalls = newCounter.getCallCount(); - console.log(`\n[REAL TIKTOKEN - User reported scenario: ~120k tokens]`); - console.log(`OLD implementation: ${oldCalls} tiktoken calls, ${timeOld.toFixed(0)}ms`); - console.log(`NEW implementation: ${newCalls} tiktoken calls, ${timeNew.toFixed(0)}ms`); + console.log(`\n[REAL TOKENIZER - ~20k tokens]`); + console.log(`OLD implementation: ${oldCalls} tokenizer calls, ${timeOld.toFixed(0)}ms`); + console.log(`NEW implementation: ${newCalls} tokenizer calls, ${timeNew.toFixed(0)}ms`); console.log(`Call reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); console.log(`Time reduction: ${((1 - timeNew / timeOld) * 100).toFixed(1)}%`); console.log( @@ -684,8 +680,8 @@ describe('processTextWithTokenLimit', () => { it('should achieve at least 70% reduction with real tokenizer', async () => { const oldCounter = createRealTokenCounter(); const newCounter = createRealTokenCounter(); - const text = createRealisticText(50000); - const tokenLimit = 10000; + const text = createRealisticText(15000); + const tokenLimit = 5000; await processTextWithTokenLimitOLD({ text, @@ -693,8 +689,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -706,7 +700,7 @@ describe('processTextWithTokenLimit', () => { const reduction = 1 - newCalls / oldCalls; console.log( - `[Real tiktoken 50k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, + `[Real tokenizer 15k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, ); expect(reduction).toBeGreaterThanOrEqual(0.7); @@ -714,10 +708,6 @@ describe('processTextWithTokenLimit', () => { }); describe('using countTokens async function from @librechat/api', () => { - beforeEach(() => { - Tokenizer.freeAndResetAllEncoders(); - }); - it('countTokens should return correct token count', async () => { const text = 'Hello, world!'; const count = await countTokens(text); @@ -759,8 +749,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -776,11 +764,11 @@ describe('processTextWithTokenLimit', () => { expect(newCalls).toBeLessThan(oldCalls); }); - it('should handle user reported scenario with countTokens (~120k tokens)', async () => { + it('should handle large text with countTokens (~20k tokens)', async () => { const oldCounter = createCountTokensCounter(); const newCounter = createCountTokensCounter(); - const text = createRealisticText(120000); - const tokenLimit = 100000; + const text = createRealisticText(20000); + const tokenLimit = 15000; const startOld = performance.now(); await processTextWithTokenLimitOLD({ @@ -790,8 +778,6 @@ describe('processTextWithTokenLimit', () => { }); const timeOld = performance.now() - startOld; - Tokenizer.freeAndResetAllEncoders(); - const startNew = performance.now(); const result = await processTextWithTokenLimit({ text, @@ -803,7 +789,7 @@ describe('processTextWithTokenLimit', () => { const oldCalls = oldCounter.getCallCount(); const newCalls = newCounter.getCallCount(); - console.log(`\n[countTokens - User reported scenario: ~120k tokens]`); + console.log(`\n[countTokens - ~20k tokens]`); console.log(`OLD implementation: ${oldCalls} countTokens calls, ${timeOld.toFixed(0)}ms`); console.log(`NEW implementation: ${newCalls} countTokens calls, ${timeNew.toFixed(0)}ms`); console.log(`Call reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); @@ -820,8 +806,8 @@ describe('processTextWithTokenLimit', () => { it('should achieve at least 70% reduction with countTokens', async () => { const oldCounter = createCountTokensCounter(); const newCounter = createCountTokensCounter(); - const text = createRealisticText(50000); - const tokenLimit = 10000; + const text = createRealisticText(15000); + const tokenLimit = 5000; await processTextWithTokenLimitOLD({ text, @@ -829,8 +815,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -842,7 +826,7 @@ describe('processTextWithTokenLimit', () => { const reduction = 1 - newCalls / oldCalls; console.log( - `[countTokens 50k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, + `[countTokens 15k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, ); expect(reduction).toBeGreaterThanOrEqual(0.7); diff --git a/packages/api/src/utils/tokenizer.spec.ts b/packages/api/src/utils/tokenizer.spec.ts index edd6fe14de..b8c1bd8d98 100644 --- a/packages/api/src/utils/tokenizer.spec.ts +++ b/packages/api/src/utils/tokenizer.spec.ts @@ -1,12 +1,3 @@ -/** - * @file Tokenizer.spec.cjs - * - * Tests the real TokenizerSingleton (no mocking of `tiktoken`). - * Make sure to install `tiktoken` and have it configured properly. - */ - -import { logger } from '@librechat/data-schemas'; -import type { Tiktoken } from 'tiktoken'; import Tokenizer from './tokenizer'; jest.mock('@librechat/data-schemas', () => ({ @@ -17,127 +8,49 @@ jest.mock('@librechat/data-schemas', () => ({ describe('Tokenizer', () => { it('should be a singleton (same instance)', async () => { - const AnotherTokenizer = await import('./tokenizer'); // same path + const AnotherTokenizer = await import('./tokenizer'); expect(Tokenizer).toBe(AnotherTokenizer.default); }); - describe('getTokenizer', () => { - it('should create an encoder for an explicit model name (e.g., "gpt-4")', () => { - // The real `encoding_for_model` will be called internally - // as soon as we pass isModelName = true. - const tokenizer = Tokenizer.getTokenizer('gpt-4', true); - - // Basic sanity checks - expect(tokenizer).toBeDefined(); - // You can optionally check certain properties from `tiktoken` if they exist - // e.g., expect(typeof tokenizer.encode).toBe('function'); + describe('initEncoding', () => { + it('should load o200k_base encoding', async () => { + await Tokenizer.initEncoding('o200k_base'); + const count = Tokenizer.getTokenCount('Hello, world!', 'o200k_base'); + expect(count).toBeGreaterThan(0); }); - it('should create an encoder for a known encoding (e.g., "cl100k_base")', () => { - // The real `get_encoding` will be called internally - // as soon as we pass isModelName = false. - const tokenizer = Tokenizer.getTokenizer('cl100k_base', false); - - expect(tokenizer).toBeDefined(); - // e.g., expect(typeof tokenizer.encode).toBe('function'); + it('should load claude encoding', async () => { + await Tokenizer.initEncoding('claude'); + const count = Tokenizer.getTokenCount('Hello, world!', 'claude'); + expect(count).toBeGreaterThan(0); }); - it('should return cached tokenizer if previously fetched', () => { - const tokenizer1 = Tokenizer.getTokenizer('cl100k_base', false); - const tokenizer2 = Tokenizer.getTokenizer('cl100k_base', false); - // Should be the exact same instance from the cache - expect(tokenizer1).toBe(tokenizer2); - }); - }); - - describe('freeAndResetAllEncoders', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should free all encoders and reset tokenizerCallsCount to 1', () => { - // By creating two different encodings, we populate the cache - Tokenizer.getTokenizer('cl100k_base', false); - Tokenizer.getTokenizer('r50k_base', false); - - // Now free them - Tokenizer.freeAndResetAllEncoders(); - - // The internal cache is cleared - expect(Tokenizer.tokenizersCache['cl100k_base']).toBeUndefined(); - expect(Tokenizer.tokenizersCache['r50k_base']).toBeUndefined(); - - // tokenizerCallsCount is reset to 1 - expect(Tokenizer.tokenizerCallsCount).toBe(1); - }); - - it('should catch and log errors if freeing fails', () => { - // Mock logger.error before the test - const mockLoggerError = jest.spyOn(logger, 'error'); - - // Set up a problematic tokenizer in the cache - Tokenizer.tokenizersCache['cl100k_base'] = { - free() { - throw new Error('Intentional free error'); - }, - } as unknown as Tiktoken; - - // Should not throw uncaught errors - Tokenizer.freeAndResetAllEncoders(); - - // Verify logger.error was called with correct arguments - expect(mockLoggerError).toHaveBeenCalledWith( - '[Tokenizer] Free and reset encoders error', - expect.any(Error), - ); - - // Clean up - mockLoggerError.mockRestore(); - Tokenizer.tokenizersCache = {}; + it('should deduplicate concurrent init calls', async () => { + const [, , count] = await Promise.all([ + Tokenizer.initEncoding('o200k_base'), + Tokenizer.initEncoding('o200k_base'), + Tokenizer.initEncoding('o200k_base').then(() => + Tokenizer.getTokenCount('test', 'o200k_base'), + ), + ]); + expect(count).toBeGreaterThan(0); }); }); describe('getTokenCount', () => { - beforeEach(() => { - jest.clearAllMocks(); - Tokenizer.freeAndResetAllEncoders(); + beforeAll(async () => { + await Tokenizer.initEncoding('o200k_base'); + await Tokenizer.initEncoding('claude'); }); it('should return the number of tokens in the given text', () => { - const text = 'Hello, world!'; - const count = Tokenizer.getTokenCount(text, 'cl100k_base'); + const count = Tokenizer.getTokenCount('Hello, world!', 'o200k_base'); expect(count).toBeGreaterThan(0); }); - it('should reset encoders if an error is thrown', () => { - // We can simulate an error by temporarily overriding the selected tokenizer's `encode` method. - const tokenizer = Tokenizer.getTokenizer('cl100k_base', false); - const originalEncode = tokenizer.encode; - tokenizer.encode = () => { - throw new Error('Forced error'); - }; - - // Despite the forced error, the code should catch and reset, then re-encode - const count = Tokenizer.getTokenCount('Hello again', 'cl100k_base'); + it('should count tokens using claude encoding', () => { + const count = Tokenizer.getTokenCount('Hello, world!', 'claude'); expect(count).toBeGreaterThan(0); - - // Restore the original encode - tokenizer.encode = originalEncode; - }); - - it('should reset tokenizers after 25 calls', () => { - // Spy on freeAndResetAllEncoders - const resetSpy = jest.spyOn(Tokenizer, 'freeAndResetAllEncoders'); - - // Make 24 calls; should NOT reset yet - for (let i = 0; i < 24; i++) { - Tokenizer.getTokenCount('test text', 'cl100k_base'); - } - expect(resetSpy).not.toHaveBeenCalled(); - - // 25th call triggers the reset - Tokenizer.getTokenCount('the 25th call!', 'cl100k_base'); - expect(resetSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/api/src/utils/tokenizer.ts b/packages/api/src/utils/tokenizer.ts index 0b0282d36b..4c638c948e 100644 --- a/packages/api/src/utils/tokenizer.ts +++ b/packages/api/src/utils/tokenizer.ts @@ -1,74 +1,46 @@ import { logger } from '@librechat/data-schemas'; -import { encoding_for_model as encodingForModel, get_encoding as getEncoding } from 'tiktoken'; -import type { Tiktoken, TiktokenModel, TiktokenEncoding } from 'tiktoken'; +import { Tokenizer as AiTokenizer } from 'ai-tokenizer'; -interface TokenizerOptions { - debug?: boolean; -} +export type EncodingName = 'o200k_base' | 'claude'; + +type EncodingData = ConstructorParameters[0]; class Tokenizer { - tokenizersCache: Record; - tokenizerCallsCount: number; - private options?: TokenizerOptions; + private tokenizersCache: Partial> = {}; + private loadingPromises: Partial>> = {}; - constructor() { - this.tokenizersCache = {}; - this.tokenizerCallsCount = 0; - } - - getTokenizer( - encoding: TiktokenModel | TiktokenEncoding, - isModelName = false, - extendSpecialTokens: Record = {}, - ): Tiktoken { - let tokenizer: Tiktoken; + /** Pre-loads an encoding so that subsequent getTokenCount calls are accurate. */ + async initEncoding(encoding: EncodingName): Promise { if (this.tokenizersCache[encoding]) { - tokenizer = this.tokenizersCache[encoding]; - } else { - if (isModelName) { - tokenizer = encodingForModel(encoding as TiktokenModel, extendSpecialTokens); - } else { - tokenizer = getEncoding(encoding as TiktokenEncoding, extendSpecialTokens); - } - this.tokenizersCache[encoding] = tokenizer; + return; } - return tokenizer; + if (this.loadingPromises[encoding]) { + return this.loadingPromises[encoding]; + } + this.loadingPromises[encoding] = (async () => { + const data: EncodingData = + encoding === 'claude' + ? await import('ai-tokenizer/encoding/claude') + : await import('ai-tokenizer/encoding/o200k_base'); + this.tokenizersCache[encoding] = new AiTokenizer(data); + })(); + return this.loadingPromises[encoding]; } - freeAndResetAllEncoders(): void { + getTokenCount(text: string, encoding: EncodingName = 'o200k_base'): number { + const tokenizer = this.tokenizersCache[encoding]; + if (!tokenizer) { + this.initEncoding(encoding); + return Math.ceil(text.length / 4); + } try { - Object.keys(this.tokenizersCache).forEach((key) => { - if (this.tokenizersCache[key]) { - this.tokenizersCache[key].free(); - delete this.tokenizersCache[key]; - } - }); - this.tokenizerCallsCount = 1; - } catch (error) { - logger.error('[Tokenizer] Free and reset encoders error', error); - } - } - - resetTokenizersIfNecessary(): void { - if (this.tokenizerCallsCount >= 25) { - if (this.options?.debug) { - logger.debug('[Tokenizer] freeAndResetAllEncoders: reached 25 encodings, resetting...'); - } - this.freeAndResetAllEncoders(); - } - this.tokenizerCallsCount++; - } - - getTokenCount(text: string, encoding: TiktokenModel | TiktokenEncoding = 'cl100k_base'): number { - this.resetTokenizersIfNecessary(); - try { - const tokenizer = this.getTokenizer(encoding); - return tokenizer.encode(text, 'all').length; + return tokenizer.count(text); } catch (error) { logger.error('[Tokenizer] Error getting token count:', error); - this.freeAndResetAllEncoders(); - const tokenizer = this.getTokenizer(encoding); - return tokenizer.encode(text, 'all').length; + delete this.tokenizersCache[encoding]; + delete this.loadingPromises[encoding]; + this.initEncoding(encoding); + return Math.ceil(text.length / 4); } } } @@ -76,13 +48,13 @@ class Tokenizer { const TokenizerSingleton = new Tokenizer(); /** - * Counts the number of tokens in a given text using tiktoken. - * This is an async wrapper around Tokenizer.getTokenCount for compatibility. - * @param text - The text to be tokenized. Defaults to an empty string if not provided. + * Counts the number of tokens in a given text using ai-tokenizer with o200k_base encoding. + * @param text - The text to count tokens in. Defaults to an empty string. * @returns The number of tokens in the provided text. */ export async function countTokens(text = ''): Promise { - return TokenizerSingleton.getTokenCount(text, 'cl100k_base'); + await TokenizerSingleton.initEncoding('o200k_base'); + return TokenizerSingleton.getTokenCount(text, 'o200k_base'); } export default TokenizerSingleton; diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 32b2fc6036..ae09da4f28 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -593,42 +593,3 @@ export function processModelData(input: z.infer): EndpointTo return tokenConfig; } - -export const tiktokenModels = new Set([ - 'text-davinci-003', - 'text-davinci-002', - 'text-davinci-001', - 'text-curie-001', - 'text-babbage-001', - 'text-ada-001', - 'davinci', - 'curie', - 'babbage', - 'ada', - 'code-davinci-002', - 'code-davinci-001', - 'code-cushman-002', - 'code-cushman-001', - 'davinci-codex', - 'cushman-codex', - 'text-davinci-edit-001', - 'code-davinci-edit-001', - 'text-embedding-ada-002', - 'text-similarity-davinci-001', - 'text-similarity-curie-001', - 'text-similarity-babbage-001', - 'text-similarity-ada-001', - 'text-search-davinci-doc-001', - 'text-search-curie-doc-001', - 'text-search-babbage-doc-001', - 'text-search-ada-doc-001', - 'code-search-babbage-code-001', - 'code-search-ada-code-001', - 'gpt2', - 'gpt-4', - 'gpt-4-0314', - 'gpt-4-32k', - 'gpt-4-32k-0314', - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0301', -]); From fc6f7a337dc66eb95f1b24e2b095a745eb36d1d2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:46:55 -0400 Subject: [PATCH 020/111] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#12176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/lv/translation.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index 76c2db24ea..57794a9e2a 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -39,7 +39,7 @@ "com_agents_description_card": "Apraksts: {{description}}", "com_agents_description_placeholder": "Pēc izvēles: aprakstiet savu aģentu šeit", "com_agents_empty_state_heading": "Nav atrasts neviens aģents", - "com_agents_enable_file_search": "Iespējot vektorizēto meklēšanu", + "com_agents_enable_file_search": "Iespējot meklēšanu dokumentos", "com_agents_error_bad_request_message": "Pieprasījumu nevarēja apstrādāt.", "com_agents_error_bad_request_suggestion": "Lūdzu, pārbaudiet ievadītos datus un mēģiniet vēlreiz.", "com_agents_error_category_title": "Kategorija Kļūda", @@ -66,7 +66,7 @@ "com_agents_file_context_description": "Visi augšupielādētie faili tiek pilnībā pārveidoti tekstā un nekavējoties pievienoti aģenta pamata kontekstam kā nemainīgs saturs, kas pieejams visu sarunas laiku. Ja augšupielādētajam faila tipam ir pieejams vai konfigurēts OCR, teksta izvilkšana notiek automātiski. Šī metode ir piemērota gadījumos, kad nepieciešams analizēt visu dokumenta, attēla ar tekstu vai PDF faila saturu, taču jāņem vērā, ka tas ievērojami palielina atmiņas patēriņu un izmaksas.", "com_agents_file_context_disabled": "Pirms failu augšupielādes, lai to pievienotu kā kontekstu, ir jāizveido aģents.", "com_agents_file_context_label": "Pievienot failu kā kontekstu", - "com_agents_file_search_disabled": "Lai varētu iespējot vektorizētu meklēšanu ir jāizveido aģents.", + "com_agents_file_search_disabled": "Lai varētu iespējot meklēšanu dokumentos ir jāizveido aģents.", "com_agents_file_search_info": "Kad šī opcija ir iespējota, aģents izmanto vektorizētu datu meklēšanu (RAG pieeju), kas ļauj efektīvi un izmaksu ziņā izdevīgi izgūt atbilstošu kontekstu tikai no būtiskākajām faila daļām, balstoties uz lietotāja jautājumu, nevis analizē visu failu pilnā apjomā.", "com_agents_grid_announcement": "Rādu {{count}} aģentus {{category}} kategorijā", "com_agents_instructions_placeholder": "Sistēmas instrukcijas, ko izmantos aģents", @@ -126,7 +126,7 @@ "com_assistants_delete_actions_success": "Darbība veiksmīgi dzēsta no asistenta", "com_assistants_description_placeholder": "Pēc izvēles: Šeit aprakstiet savu asistentu", "com_assistants_domain_info": "Asistents nosūtīja šo informāciju {{0}}", - "com_assistants_file_search": "Vektorizētā Meklēšana (RAG)", + "com_assistants_file_search": "Meklēšana dokumentos", "com_assistants_file_search_info": "Šī funkcija ļauj asistentam izmantot augšupielādēto failu saturu, pievienojot zināšanas tieši no lietotāja vai citu lietotāju failiem. Pēc faila augšupielādes asistents automātiski identificē un izgūst nepieciešamās teksta daļas atbilstoši lietotāja pieprasījumam, neiekļaujot visu failu pilnā apjomā. Vektoru datubāzu (vector store) pieslēgšana tieši šai funkcijai šobrīd nav atbalstīta; tās iespējams pievienot tikai Provider Playground vidē vai augšupielādējot failus sarunas pavedienam ikreizējai meklēšanai.", "com_assistants_function_use": "Izmantotais asistents {{0}}", "com_assistants_image_vision": "Attēla redzējums", @@ -136,7 +136,7 @@ "com_assistants_knowledge_info": "Ja augšupielādējat failus sadaļā Zināšanas, sarunās ar asistentu var tikt iekļauts faila saturs.", "com_assistants_max_starters_reached": "Sasniegts maksimālais sarunu uzsākšanas iespēju skaits", "com_assistants_name_placeholder": "Pēc izvēles: Asistenta nosaukums", - "com_assistants_non_retrieval_model": "Šajā modelī vektorizētā meklēšana nav iespējota. Lūdzu, izvēlieties citu modeli.", + "com_assistants_non_retrieval_model": "Šajā modelī meklēšana dokumentos nav iespējota. Lūdzu, izvēlieties citu modeli.", "com_assistants_retrieval": "Atgūšana", "com_assistants_running_action": "Darbība palaista", "com_assistants_running_var": "Strādā {{0}}", @@ -232,7 +232,7 @@ "com_endpoint_anthropic_thinking_budget": "Nosaka maksimālo žetonu skaitu, ko Claude drīkst izmantot savā iekšējā spriešanas procesā. Lielāki budžeti var uzlabot atbilžu kvalitāti, nodrošinot rūpīgāku analīzi sarežģītām problēmām, lai gan Claude var neizmantot visu piešķirto budžetu, īpaši diapazonos virs 32 000. Šim iestatījumam jābūt zemākam par \"Maksimālie izvades tokeni\".", "com_endpoint_anthropic_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).", "com_endpoint_anthropic_topp": "`Top-p` maina to, kā modelis atlasa marķierus izvadei. Marķieri tiek atlasīti no K (skatīt parametru topK) ticamākās līdz vismazāk ticamajai, līdz to varbūtību summa ir vienāda ar `top-p` vērtību.", - "com_endpoint_anthropic_use_web_search": "Iespējojiet tīmekļa meklēšanas funkcionalitāti, izmantojot Anthropic iebūvētās meklēšanas iespējas. Tas ļauj modelim meklēt tīmeklī jaunāko informāciju un sniegt precīzākas un aktuālākas atbildes.", + "com_endpoint_anthropic_use_web_search": "Iespējojiet meklēšanu tīmeklī funkcionalitāti, izmantojot Anthropic iebūvētās meklēšanas iespējas. Tas ļauj modelim meklēt tīmeklī jaunāko informāciju un sniegt precīzākas un aktuālākas atbildes.", "com_endpoint_assistant": "Asistents", "com_endpoint_assistant_model": "Asistenta modelis", "com_endpoint_assistant_placeholder": "Lūdzu, labajā sānu panelī atlasiet asistentu.", @@ -1486,7 +1486,7 @@ "com_ui_version_var": "Versija {{0}}", "com_ui_versions": "Versijas", "com_ui_view_memory": "Skatīt atmiņu", - "com_ui_web_search": "Tīmekļa meklēšana", + "com_ui_web_search": "Meklēšana tīmeklī", "com_ui_web_search_cohere_key": "Ievadiet Cohere API atslēgu", "com_ui_web_search_firecrawl_url": "Firecrawl API URL (pēc izvēles)", "com_ui_web_search_jina_key": "Ievadiet Jina API atslēgu", From 3ddf62c8e5511a2c30672dbe3e6e07bedff374e6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 12 Mar 2026 20:43:23 -0400 Subject: [PATCH 021/111] =?UTF-8?q?=F0=9F=AB=99=20fix:=20Force=20MeiliSear?= =?UTF-8?q?ch=20Full=20Sync=20on=20Empty=20Index=20State=20(#12202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: meili index sync with unindexed documents - Updated `performSync` function to force a full sync when a fresh MeiliSearch index is detected, even if the number of unindexed messages or convos is below the sync threshold. - Added logging to indicate when a fresh index is detected and a full sync is initiated. - Introduced new tests to validate the behavior of the sync logic under various conditions, ensuring proper handling of fresh indexes and threshold scenarios. This change improves the reliability of the synchronization process, ensuring that all documents are indexed correctly when starting with a fresh index. * refactor: update sync logic for unindexed documents in MeiliSearch - Renamed variables in `performSync` to improve clarity, changing `freshIndex` to `noneIndexed` for better understanding of the sync condition. - Adjusted the logic to ensure a full sync is forced when no messages or conversations are marked as indexed, even if below the sync threshold. - Updated related tests to reflect the new logging messages and conditions, enhancing the accuracy of the sync threshold logic. This change improves the readability and reliability of the synchronization process, ensuring all documents are indexed correctly when starting with a fresh index. * fix: enhance MeiliSearch index creation error handling - Updated the `mongoMeili` function to improve logging and error handling during index creation in MeiliSearch. - Added handling for `MeiliSearchTimeOutError` to log a warning when index creation times out. - Enhanced logging to differentiate between successful index creation and specific failure reasons, including cases where the index already exists. - Improved debug logging for index creation tasks to provide clearer insights into the process. This change enhances the robustness of the index creation process and improves observability for troubleshooting. * fix: update MeiliSearch index creation error handling - Modified the `mongoMeili` function to check for any status other than 'succeeded' during index creation, enhancing error detection. - Improved logging to provide clearer insights when an index creation task fails, particularly for cases where the index already exists. This change strengthens the error handling mechanism for index creation in MeiliSearch, ensuring better observability and reliability. --- api/db/indexSync.js | 14 +++- api/db/indexSync.spec.js | 65 +++++++++++++++++++ .../src/models/plugins/mongoMeili.ts | 31 ++++++--- 3 files changed, 99 insertions(+), 11 deletions(-) diff --git a/api/db/indexSync.js b/api/db/indexSync.js index 8e8e999d92..130cde77b8 100644 --- a/api/db/indexSync.js +++ b/api/db/indexSync.js @@ -236,8 +236,12 @@ async function performSync(flowManager, flowId, flowType) { const messageCount = messageProgress.totalDocuments; const messagesIndexed = messageProgress.totalProcessed; const unindexedMessages = messageCount - messagesIndexed; + const noneIndexed = messagesIndexed === 0 && unindexedMessages > 0; - if (settingsUpdated || unindexedMessages > syncThreshold) { + if (settingsUpdated || noneIndexed || unindexedMessages > syncThreshold) { + if (noneIndexed && !settingsUpdated) { + logger.info('[indexSync] No messages marked as indexed, forcing full sync'); + } logger.info(`[indexSync] Starting message sync (${unindexedMessages} unindexed)`); await Message.syncWithMeili(); messagesSync = true; @@ -261,9 +265,13 @@ async function performSync(flowManager, flowId, flowType) { const convoCount = convoProgress.totalDocuments; const convosIndexed = convoProgress.totalProcessed; - const unindexedConvos = convoCount - convosIndexed; - if (settingsUpdated || unindexedConvos > syncThreshold) { + const noneConvosIndexed = convosIndexed === 0 && unindexedConvos > 0; + + if (settingsUpdated || noneConvosIndexed || unindexedConvos > syncThreshold) { + if (noneConvosIndexed && !settingsUpdated) { + logger.info('[indexSync] No conversations marked as indexed, forcing full sync'); + } logger.info(`[indexSync] Starting convos sync (${unindexedConvos} unindexed)`); await Conversation.syncWithMeili(); convosSync = true; diff --git a/api/db/indexSync.spec.js b/api/db/indexSync.spec.js index c2e5901d6a..dbe07c7595 100644 --- a/api/db/indexSync.spec.js +++ b/api/db/indexSync.spec.js @@ -462,4 +462,69 @@ describe('performSync() - syncThreshold logic', () => { ); expect(mockLogger.info).toHaveBeenCalledWith('[indexSync] Starting convos sync (50 unindexed)'); }); + + test('forces sync when zero documents indexed (reset scenario) even if below threshold', async () => { + Message.getSyncProgress.mockResolvedValue({ + totalProcessed: 0, + totalDocuments: 680, + isComplete: false, + }); + + Conversation.getSyncProgress.mockResolvedValue({ + totalProcessed: 0, + totalDocuments: 76, + isComplete: false, + }); + + Message.syncWithMeili.mockResolvedValue(undefined); + Conversation.syncWithMeili.mockResolvedValue(undefined); + + const indexSync = require('./indexSync'); + await indexSync(); + + expect(Message.syncWithMeili).toHaveBeenCalledTimes(1); + expect(Conversation.syncWithMeili).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] No messages marked as indexed, forcing full sync', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] Starting message sync (680 unindexed)', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] No conversations marked as indexed, forcing full sync', + ); + expect(mockLogger.info).toHaveBeenCalledWith('[indexSync] Starting convos sync (76 unindexed)'); + }); + + test('does NOT force sync when some documents already indexed and below threshold', async () => { + Message.getSyncProgress.mockResolvedValue({ + totalProcessed: 630, + totalDocuments: 680, + isComplete: false, + }); + + Conversation.getSyncProgress.mockResolvedValue({ + totalProcessed: 70, + totalDocuments: 76, + isComplete: false, + }); + + const indexSync = require('./indexSync'); + await indexSync(); + + expect(Message.syncWithMeili).not.toHaveBeenCalled(); + expect(Conversation.syncWithMeili).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalledWith( + '[indexSync] No messages marked as indexed, forcing full sync', + ); + expect(mockLogger.info).not.toHaveBeenCalledWith( + '[indexSync] No conversations marked as indexed, forcing full sync', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] 50 messages unindexed (below threshold: 1000, skipping)', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] 6 convos unindexed (below threshold: 1000, skipping)', + ); + }); }); diff --git a/packages/data-schemas/src/models/plugins/mongoMeili.ts b/packages/data-schemas/src/models/plugins/mongoMeili.ts index 66530e2aba..cc01dbb6c7 100644 --- a/packages/data-schemas/src/models/plugins/mongoMeili.ts +++ b/packages/data-schemas/src/models/plugins/mongoMeili.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; -import { MeiliSearch } from 'meilisearch'; import { parseTextParts } from 'librechat-data-provider'; -import type { SearchResponse, SearchParams, Index } from 'meilisearch'; +import { MeiliSearch, MeiliSearchTimeOutError } from 'meilisearch'; +import type { SearchResponse, SearchParams, Index, MeiliSearchErrorInfo } from 'meilisearch'; import type { CallbackWithoutResultAndOptionalError, FilterQuery, @@ -581,7 +581,6 @@ export default function mongoMeili(schema: Schema, options: MongoMeiliOptions): /** Create index only if it doesn't exist */ const index = client.index(indexName); - // Check if index exists and create if needed (async () => { try { await index.getRawInfo(); @@ -591,18 +590,34 @@ export default function mongoMeili(schema: Schema, options: MongoMeiliOptions): if (errorCode === 'index_not_found') { try { logger.info(`[mongoMeili] Creating new index: ${indexName}`); - await client.createIndex(indexName, { primaryKey }); - logger.info(`[mongoMeili] Successfully created index: ${indexName}`); + const enqueued = await client.createIndex(indexName, { primaryKey }); + const task = await client.waitForTask(enqueued.taskUid, { + timeOutMs: 10000, + intervalMs: 100, + }); + logger.debug(`[mongoMeili] Index ${indexName} creation task:`, task); + if (task.status !== 'succeeded') { + const taskError = task.error as MeiliSearchErrorInfo | null; + if (taskError?.code === 'index_already_exists') { + logger.debug(`[mongoMeili] Index ${indexName} was created by another instance`); + } else { + logger.warn(`[mongoMeili] Index ${indexName} creation failed:`, taskError); + } + } else { + logger.info(`[mongoMeili] Successfully created index: ${indexName}`); + } } catch (createError) { - // Index might have been created by another instance - logger.debug(`[mongoMeili] Index ${indexName} may already exist:`, createError); + if (createError instanceof MeiliSearchTimeOutError) { + logger.warn(`[mongoMeili] Timed out waiting for index ${indexName} creation`); + } else { + logger.warn(`[mongoMeili] Error creating index ${indexName}:`, createError); + } } } else { logger.error(`[mongoMeili] Error checking index ${indexName}:`, error); } } - // Configure index settings to make 'user' field filterable try { await index.updateSettings({ filterableAttributes: ['user'], From 65b0bfde1b07141b4d2fefd246379c6102b5fed4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:48:05 -0400 Subject: [PATCH 022/111] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#12203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/fr/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index c9d78ac3f5..7838b33739 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -1203,7 +1203,7 @@ "com_ui_upload_image_input": "Téléverser une image", "com_ui_upload_invalid": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser la limite", "com_ui_upload_invalid_var": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser {{0}} Mo", - "com_ui_upload_ocr_text": "Téléchager en tant que texte", + "com_ui_upload_ocr_text": "Télécharger en tant que texte", "com_ui_upload_provider": "Télécharger vers le fournisseur", "com_ui_upload_success": "Fichier téléversé avec succès", "com_ui_upload_type": "Sélectionner le type de téléversement", From f32907cd362c9d87b362661cd30a8f8a718fc864 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 12 Mar 2026 23:19:31 -0400 Subject: [PATCH 023/111] =?UTF-8?q?=F0=9F=94=8F=20fix:=20MCP=20Server=20UR?= =?UTF-8?q?L=20Schema=20Validation=20(#12204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: MCP server configuration validation and schema - Added tests to reject URLs containing environment variable references for SSE, streamable-http, and websocket types in the MCP routes. - Introduced a new schema in the data provider to ensure user input URLs do not resolve environment variables, enhancing security against potential leaks. - Updated existing MCP server user input schema to utilize the new validation logic, ensuring consistent handling of user-supplied URLs across the application. * fix: MCP URL validation to reject env variable references - Updated tests to ensure that URLs for SSE, streamable-http, and websocket types containing environment variable patterns are rejected, improving security against potential leaks. - Refactored the MCP server user input schema to enforce stricter validation rules, preventing the resolution of environment variables in user-supplied URLs. - Introduced new test cases for various URL types to validate the rejection logic, ensuring consistent handling across the application. * test: Enhance MCPServerUserInputSchema tests for environment variable handling - Introduced new test cases to validate the prevention of environment variable exfiltration through user input URLs in the MCPServerUserInputSchema. - Updated existing tests to confirm that URLs containing environment variable patterns are correctly resolved or rejected, improving security against potential leaks. - Refactored test structure to better organize environment variable handling scenarios, ensuring comprehensive coverage of edge cases. --- api/server/routes/__tests__/mcp.spec.js | 90 ++++++++++++++ packages/data-provider/specs/mcp.spec.ts | 147 +++++++++++++++++++++++ packages/data-provider/src/mcp.ts | 35 +++++- 3 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 packages/data-provider/specs/mcp.spec.ts diff --git a/api/server/routes/__tests__/mcp.spec.js b/api/server/routes/__tests__/mcp.spec.js index 009b602604..e0cb680169 100644 --- a/api/server/routes/__tests__/mcp.spec.js +++ b/api/server/routes/__tests__/mcp.spec.js @@ -1819,6 +1819,51 @@ describe('MCP Routes', () => { expect(response.body.message).toBe('Invalid configuration'); }); + it('should reject SSE URL containing env variable references', async () => { + const response = await request(app) + .post('/api/mcp/servers') + .send({ + config: { + type: 'sse', + url: 'http://attacker.com/?secret=${JWT_SECRET}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.addServer).not.toHaveBeenCalled(); + }); + + it('should reject streamable-http URL containing env variable references', async () => { + const response = await request(app) + .post('/api/mcp/servers') + .send({ + config: { + type: 'streamable-http', + url: 'http://attacker.com/?key=${CREDS_KEY}&iv=${CREDS_IV}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.addServer).not.toHaveBeenCalled(); + }); + + it('should reject websocket URL containing env variable references', async () => { + const response = await request(app) + .post('/api/mcp/servers') + .send({ + config: { + type: 'websocket', + url: 'ws://attacker.com/?secret=${MONGO_URI}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.addServer).not.toHaveBeenCalled(); + }); + it('should return 500 when registry throws error', async () => { const validConfig = { type: 'sse', @@ -1918,6 +1963,51 @@ describe('MCP Routes', () => { expect(response.body.errors).toBeDefined(); }); + it('should reject SSE URL containing env variable references', async () => { + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ + config: { + type: 'sse', + url: 'http://attacker.com/?secret=${JWT_SECRET}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.updateServer).not.toHaveBeenCalled(); + }); + + it('should reject streamable-http URL containing env variable references', async () => { + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ + config: { + type: 'streamable-http', + url: 'http://attacker.com/?key=${CREDS_KEY}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.updateServer).not.toHaveBeenCalled(); + }); + + it('should reject websocket URL containing env variable references', async () => { + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ + config: { + type: 'websocket', + url: 'ws://attacker.com/?secret=${MONGO_URI}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.updateServer).not.toHaveBeenCalled(); + }); + it('should return 500 when registry throws error', async () => { const validConfig = { type: 'sse', diff --git a/packages/data-provider/specs/mcp.spec.ts b/packages/data-provider/specs/mcp.spec.ts new file mode 100644 index 0000000000..573769c4fa --- /dev/null +++ b/packages/data-provider/specs/mcp.spec.ts @@ -0,0 +1,147 @@ +import { SSEOptionsSchema, MCPServerUserInputSchema } from '../src/mcp'; + +describe('MCPServerUserInputSchema', () => { + describe('env variable exfiltration prevention', () => { + it('should confirm admin schema resolves env vars (attack vector baseline)', () => { + process.env.FAKE_SECRET = 'leaked-secret-value'; + const adminResult = SSEOptionsSchema.safeParse({ + type: 'sse', + url: 'http://attacker.com/?secret=${FAKE_SECRET}', + }); + expect(adminResult.success).toBe(true); + if (adminResult.success) { + expect(adminResult.data.url).toContain('leaked-secret-value'); + } + delete process.env.FAKE_SECRET; + }); + + it('should reject the same URL through user input schema', () => { + process.env.FAKE_SECRET = 'leaked-secret-value'; + const userResult = MCPServerUserInputSchema.safeParse({ + type: 'sse', + url: 'http://attacker.com/?secret=${FAKE_SECRET}', + }); + expect(userResult.success).toBe(false); + delete process.env.FAKE_SECRET; + }); + }); + + describe('env variable rejection', () => { + it('should reject SSE URLs containing env variable patterns', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'sse', + url: 'http://attacker.com/?secret=${FAKE_SECRET}', + }); + expect(result.success).toBe(false); + }); + + it('should reject streamable-http URLs containing env variable patterns', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'streamable-http', + url: 'http://attacker.com/?jwt=${JWT_SECRET}', + }); + expect(result.success).toBe(false); + }); + + it('should reject WebSocket URLs containing env variable patterns', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'websocket', + url: 'ws://attacker.com/?secret=${FAKE_SECRET}', + }); + expect(result.success).toBe(false); + }); + }); + + describe('protocol allowlisting', () => { + it('should reject file:// URLs for SSE', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'sse', + url: 'file:///etc/passwd', + }); + expect(result.success).toBe(false); + }); + + it('should reject ftp:// URLs for streamable-http', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'streamable-http', + url: 'ftp://internal-server/data', + }); + expect(result.success).toBe(false); + }); + + it('should reject http:// URLs for WebSocket', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'websocket', + url: 'http://example.com/ws', + }); + expect(result.success).toBe(false); + }); + + it('should reject ws:// URLs for SSE', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'sse', + url: 'ws://example.com/sse', + }); + expect(result.success).toBe(false); + }); + }); + + describe('valid URL acceptance', () => { + it('should accept valid https:// SSE URLs', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'sse', + url: 'https://mcp-server.com/sse', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.url).toBe('https://mcp-server.com/sse'); + } + }); + + it('should accept valid http:// SSE URLs', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'sse', + url: 'http://mcp-server.com/sse', + }); + expect(result.success).toBe(true); + }); + + it('should accept valid wss:// WebSocket URLs', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'websocket', + url: 'wss://mcp-server.com/ws', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.url).toBe('wss://mcp-server.com/ws'); + } + }); + + it('should accept valid ws:// WebSocket URLs', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'websocket', + url: 'ws://mcp-server.com/ws', + }); + expect(result.success).toBe(true); + }); + + it('should accept valid https:// streamable-http URLs', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'streamable-http', + url: 'https://mcp-server.com/http', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.url).toBe('https://mcp-server.com/http'); + } + }); + + it('should accept valid http:// streamable-http URLs with "http" alias', () => { + const result = MCPServerUserInputSchema.safeParse({ + type: 'http', + url: 'http://mcp-server.com/mcp', + }); + expect(result.success).toBe(true); + }); + }); +}); diff --git a/packages/data-provider/src/mcp.ts b/packages/data-provider/src/mcp.ts index 3911e91ed0..3ad296c4ec 100644 --- a/packages/data-provider/src/mcp.ts +++ b/packages/data-provider/src/mcp.ts @@ -223,6 +223,23 @@ const omitServerManagedFields = >(schema: T oauth_headers: true, }); +const envVarPattern = /\$\{[^}]+\}/; +const isWsProtocol = (val: string): boolean => /^wss?:/i.test(val); +const isHttpProtocol = (val: string): boolean => /^https?:/i.test(val); + +/** + * Builds a URL schema for user input that rejects ${VAR} env variable patterns + * and validates protocol constraints without resolving environment variables. + */ +const userUrlSchema = (protocolCheck: (val: string) => boolean, message: string) => + z + .string() + .refine((val) => !envVarPattern.test(val), { + message: 'Environment variable references are not allowed in URLs', + }) + .pipe(z.string().url()) + .refine(protocolCheck, { message }); + /** * MCP Server configuration that comes from UI/API input only. * Omits server-managed fields like startup, timeout, customUserVars, etc. @@ -232,11 +249,23 @@ const omitServerManagedFields = >(schema: T * Stdio allows arbitrary command execution and should only be configured * by administrators via the YAML config file (librechat.yaml). * Only remote transports (SSE, HTTP, WebSocket) are allowed via the API. + * + * SECURITY: URL fields use userUrlSchema instead of the admin schemas' + * extractEnvVariable transform to prevent env variable exfiltration + * through user-controlled URLs (e.g. http://attacker.com/?k=${JWT_SECRET}). + * Protocol checks use positive allowlists (http(s) / ws(s)) to block + * file://, ftp://, javascript:, and other non-network schemes. */ export const MCPServerUserInputSchema = z.union([ - omitServerManagedFields(WebSocketOptionsSchema), - omitServerManagedFields(SSEOptionsSchema), - omitServerManagedFields(StreamableHTTPOptionsSchema), + omitServerManagedFields(WebSocketOptionsSchema).extend({ + url: userUrlSchema(isWsProtocol, 'WebSocket URL must use ws:// or wss://'), + }), + omitServerManagedFields(SSEOptionsSchema).extend({ + url: userUrlSchema(isHttpProtocol, 'SSE URL must use http:// or https://'), + }), + omitServerManagedFields(StreamableHTTPOptionsSchema).extend({ + url: userUrlSchema(isHttpProtocol, 'Streamable HTTP URL must use http:// or https://'), + }), ]); export type MCPServerUserInput = z.infer; From fa9e1b228a09fb02541068902635a97686eb32cc Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 13 Mar 2026 23:18:56 -0400 Subject: [PATCH 024/111] =?UTF-8?q?=F0=9F=AA=AA=20fix:=20MCP=20API=20Respo?= =?UTF-8?q?nses=20and=20OAuth=20Validation=20(#12217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 fix: Validate MCP Configs in Server Responses * 🔒 fix: Enhance OAuth URL Validation in MCPOAuthHandler - Introduced validation for OAuth URLs to ensure they do not target private or internal addresses, enhancing security against SSRF attacks. - Updated the OAuth flow to validate both authorization and token URLs before use, ensuring compliance with security standards. - Refactored redirect URI handling to streamline the OAuth client registration process. - Added comprehensive error handling for invalid URLs, improving robustness in OAuth interactions. * 🔒 feat: Implement Permission Checks for MCP Server Management - Added permission checkers for MCP server usage and creation, enhancing access control. - Updated routes for reinitializing MCP servers and retrieving authentication values to include these permission checks, ensuring only authorized users can access these functionalities. - Refactored existing permission logic to improve clarity and maintainability. * 🔒 fix: Enhance MCP Server Response Validation and Redaction - Updated MCP route tests to use `toMatchObject` for better validation of server response structures, ensuring consistency in expected properties. - Refactored the `redactServerSecrets` function to streamline the removal of sensitive information, ensuring that user-sourced API keys are properly redacted while retaining their source. - Improved OAuth security tests to validate rejection of private URLs across multiple endpoints, enhancing protection against SSRF vulnerabilities. - Added comprehensive tests for the `redactServerSecrets` function to ensure proper handling of various server configurations, reinforcing security measures. * chore: eslint * 🔒 fix: Enhance OAuth Server URL Validation in MCPOAuthHandler - Added validation for discovered authorization server URLs to ensure they meet security standards. - Improved logging to provide clearer insights when an authorization server is found from resource metadata. - Refactored the handling of authorization server URLs to enhance robustness against potential security vulnerabilities. * 🔒 test: Bypass SSRF validation for MCP OAuth Flow tests - Mocked SSRF validation functions to allow tests to use real local HTTP servers, facilitating more accurate testing of the MCP OAuth flow. - Updated test setup to ensure compatibility with the new mocking strategy, enhancing the reliability of the tests. * 🔒 fix: Add Validation for OAuth Metadata Endpoints in MCPOAuthHandler - Implemented checks for the presence and validity of registration and token endpoints in the OAuth metadata, enhancing security by ensuring that these URLs are properly validated before use. - Improved error handling and logging to provide better insights during the OAuth metadata processing, reinforcing the robustness of the OAuth flow. * 🔒 refactor: Simplify MCP Auth Values Endpoint Logic - Removed redundant permission checks for accessing the MCP server resource in the auth-values endpoint, streamlining the request handling process. - Consolidated error handling and response structure for improved clarity and maintainability. - Enhanced logging for better insights during the authentication value checks, reinforcing the robustness of the endpoint. * 🔒 test: Refactor LeaderElection Integration Tests for Improved Cleanup - Moved Redis key cleanup to the beforeEach hook to ensure a clean state before each test. - Enhanced afterEach logic to handle instance resignations and Redis key deletion more robustly, improving test reliability and maintainability. --- api/server/controllers/mcp.js | 14 +- api/server/routes/__tests__/mcp.spec.js | 118 ++++++++- api/server/routes/mcp.js | 143 +++++------ .../LeaderElection.cache_integration.spec.ts | 18 +- .../src/mcp/__tests__/MCPOAuthFlow.test.ts | 7 + .../mcp/__tests__/MCPOAuthSecurity.test.ts | 228 ++++++++++++++++++ packages/api/src/mcp/__tests__/utils.test.ts | 201 ++++++++++++++- packages/api/src/mcp/oauth/handler.ts | 60 ++++- .../__tests__/ServerConfigsDB.test.ts | 98 ++++++++ packages/api/src/mcp/utils.ts | 60 +++++ 10 files changed, 845 insertions(+), 102 deletions(-) create mode 100644 packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js index e5dfff61ca..729f01da9d 100644 --- a/api/server/controllers/mcp.js +++ b/api/server/controllers/mcp.js @@ -7,9 +7,11 @@ */ const { logger } = require('@librechat/data-schemas'); const { + MCPErrorCodes, + redactServerSecrets, + redactAllServerSecrets, isMCPDomainNotAllowedError, isMCPInspectionFailedError, - MCPErrorCodes, } = require('@librechat/api'); const { Constants, MCPServerUserInputSchema } = require('librechat-data-provider'); const { cacheMCPServerTools, getMCPServerTools } = require('~/server/services/Config'); @@ -181,10 +183,8 @@ const getMCPServersList = async (req, res) => { return res.status(401).json({ message: 'Unauthorized' }); } - // 2. Get all server configs from registry (YAML + DB) const serverConfigs = await getMCPServersRegistry().getAllServerConfigs(userId); - - return res.json(serverConfigs); + return res.json(redactAllServerSecrets(serverConfigs)); } catch (error) { logger.error('[getMCPServersList]', error); res.status(500).json({ error: error.message }); @@ -215,7 +215,7 @@ const createMCPServerController = async (req, res) => { ); res.status(201).json({ serverName: result.serverName, - ...result.config, + ...redactServerSecrets(result.config), }); } catch (error) { logger.error('[createMCPServer]', error); @@ -243,7 +243,7 @@ const getMCPServerById = async (req, res) => { return res.status(404).json({ message: 'MCP server not found' }); } - res.status(200).json(parsedConfig); + res.status(200).json(redactServerSecrets(parsedConfig)); } catch (error) { logger.error('[getMCPServerById]', error); res.status(500).json({ message: error.message }); @@ -274,7 +274,7 @@ const updateMCPServerController = async (req, res) => { userId, ); - res.status(200).json(parsedConfig); + res.status(200).json(redactServerSecrets(parsedConfig)); } catch (error) { logger.error('[updateMCPServer]', error); const mcpErrorResponse = handleMCPError(error, res); diff --git a/api/server/routes/__tests__/mcp.spec.js b/api/server/routes/__tests__/mcp.spec.js index e0cb680169..1ad8cac087 100644 --- a/api/server/routes/__tests__/mcp.spec.js +++ b/api/server/routes/__tests__/mcp.spec.js @@ -1693,12 +1693,14 @@ describe('MCP Routes', () => { it('should return all server configs for authenticated user', async () => { const mockServerConfigs = { 'server-1': { - endpoint: 'http://server1.com', - name: 'Server 1', + type: 'sse', + url: 'http://server1.com/sse', + title: 'Server 1', }, 'server-2': { - endpoint: 'http://server2.com', - name: 'Server 2', + type: 'sse', + url: 'http://server2.com/sse', + title: 'Server 2', }, }; @@ -1707,7 +1709,18 @@ describe('MCP Routes', () => { const response = await request(app).get('/api/mcp/servers'); expect(response.status).toBe(200); - expect(response.body).toEqual(mockServerConfigs); + expect(response.body['server-1']).toMatchObject({ + type: 'sse', + url: 'http://server1.com/sse', + title: 'Server 1', + }); + expect(response.body['server-2']).toMatchObject({ + type: 'sse', + url: 'http://server2.com/sse', + title: 'Server 2', + }); + expect(response.body['server-1'].headers).toBeUndefined(); + expect(response.body['server-2'].headers).toBeUndefined(); expect(mockRegistryInstance.getAllServerConfigs).toHaveBeenCalledWith('test-user-id'); }); @@ -1762,10 +1775,10 @@ describe('MCP Routes', () => { const response = await request(app).post('/api/mcp/servers').send({ config: validConfig }); expect(response.status).toBe(201); - expect(response.body).toEqual({ - serverName: 'test-sse-server', - ...validConfig, - }); + expect(response.body.serverName).toBe('test-sse-server'); + expect(response.body.type).toBe('sse'); + expect(response.body.url).toBe('https://mcp-server.example.com/sse'); + expect(response.body.title).toBe('Test SSE Server'); expect(mockRegistryInstance.addServer).toHaveBeenCalledWith( 'temp_server_name', expect.objectContaining({ @@ -1864,6 +1877,33 @@ describe('MCP Routes', () => { expect(mockRegistryInstance.addServer).not.toHaveBeenCalled(); }); + it('should redact secrets from create response', async () => { + const validConfig = { + type: 'sse', + url: 'https://mcp-server.example.com/sse', + title: 'Test Server', + }; + + mockRegistryInstance.addServer.mockResolvedValue({ + serverName: 'test-server', + config: { + ...validConfig, + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'admin-secret-key' }, + oauth: { client_id: 'cid', client_secret: 'admin-oauth-secret' }, + headers: { Authorization: 'Bearer leaked-token' }, + }, + }); + + const response = await request(app).post('/api/mcp/servers').send({ config: validConfig }); + + expect(response.status).toBe(201); + expect(response.body.apiKey?.key).toBeUndefined(); + expect(response.body.oauth?.client_secret).toBeUndefined(); + expect(response.body.headers).toBeUndefined(); + expect(response.body.apiKey?.source).toBe('admin'); + expect(response.body.oauth?.client_id).toBe('cid'); + }); + it('should return 500 when registry throws error', async () => { const validConfig = { type: 'sse', @@ -1893,7 +1933,9 @@ describe('MCP Routes', () => { const response = await request(app).get('/api/mcp/servers/test-server'); expect(response.status).toBe(200); - expect(response.body).toEqual(mockConfig); + expect(response.body.type).toBe('sse'); + expect(response.body.url).toBe('https://mcp-server.example.com/sse'); + expect(response.body.title).toBe('Test Server'); expect(mockRegistryInstance.getServerConfig).toHaveBeenCalledWith( 'test-server', 'test-user-id', @@ -1909,6 +1951,29 @@ describe('MCP Routes', () => { expect(response.body).toEqual({ message: 'MCP server not found' }); }); + it('should redact secrets from get response', async () => { + mockRegistryInstance.getServerConfig.mockResolvedValue({ + type: 'sse', + url: 'https://mcp-server.example.com/sse', + title: 'Secret Server', + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'decrypted-admin-key' }, + oauth: { client_id: 'cid', client_secret: 'decrypted-oauth-secret' }, + headers: { Authorization: 'Bearer internal-token' }, + oauth_headers: { 'X-OAuth': 'secret-value' }, + }); + + const response = await request(app).get('/api/mcp/servers/secret-server'); + + expect(response.status).toBe(200); + expect(response.body.title).toBe('Secret Server'); + expect(response.body.apiKey?.key).toBeUndefined(); + expect(response.body.apiKey?.source).toBe('admin'); + expect(response.body.oauth?.client_secret).toBeUndefined(); + expect(response.body.oauth?.client_id).toBe('cid'); + expect(response.body.headers).toBeUndefined(); + expect(response.body.oauth_headers).toBeUndefined(); + }); + it('should return 500 when registry throws error', async () => { mockRegistryInstance.getServerConfig.mockRejectedValue(new Error('Database error')); @@ -1935,7 +2000,9 @@ describe('MCP Routes', () => { .send({ config: updatedConfig }); expect(response.status).toBe(200); - expect(response.body).toEqual(updatedConfig); + expect(response.body.type).toBe('sse'); + expect(response.body.url).toBe('https://updated-mcp-server.example.com/sse'); + expect(response.body.title).toBe('Updated Server'); expect(mockRegistryInstance.updateServer).toHaveBeenCalledWith( 'test-server', expect.objectContaining({ @@ -1947,6 +2014,35 @@ describe('MCP Routes', () => { ); }); + it('should redact secrets from update response', async () => { + const validConfig = { + type: 'sse', + url: 'https://mcp-server.example.com/sse', + title: 'Updated Server', + }; + + mockRegistryInstance.updateServer.mockResolvedValue({ + ...validConfig, + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'preserved-admin-key' }, + oauth: { client_id: 'cid', client_secret: 'preserved-oauth-secret' }, + headers: { Authorization: 'Bearer internal-token' }, + env: { DATABASE_URL: 'postgres://admin:pass@localhost/db' }, + }); + + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ config: validConfig }); + + expect(response.status).toBe(200); + expect(response.body.title).toBe('Updated Server'); + expect(response.body.apiKey?.key).toBeUndefined(); + expect(response.body.apiKey?.source).toBe('admin'); + expect(response.body.oauth?.client_secret).toBeUndefined(); + expect(response.body.oauth?.client_id).toBe('cid'); + expect(response.body.headers).toBeUndefined(); + expect(response.body.env).toBeUndefined(); + }); + it('should return 400 for invalid configuration', async () => { const invalidConfig = { type: 'sse', diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 0afac81192..57a99d199a 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -50,6 +50,18 @@ const router = Router(); const OAUTH_CSRF_COOKIE_PATH = '/api/mcp'; +const checkMCPUsePermissions = generateCheckAccess({ + permissionType: PermissionTypes.MCP_SERVERS, + permissions: [Permissions.USE], + getRoleByName, +}); + +const checkMCPCreate = generateCheckAccess({ + permissionType: PermissionTypes.MCP_SERVERS, + permissions: [Permissions.USE, Permissions.CREATE], + getRoleByName, +}); + /** * Get all MCP tools available to the user * Returns only MCP tools, completely decoupled from regular LibreChat tools @@ -470,69 +482,75 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => { * Reinitialize MCP server * This endpoint allows reinitializing a specific MCP server */ -router.post('/:serverName/reinitialize', requireJwtAuth, setOAuthSession, async (req, res) => { - try { - const { serverName } = req.params; - const user = createSafeUser(req.user); +router.post( + '/:serverName/reinitialize', + requireJwtAuth, + checkMCPUsePermissions, + setOAuthSession, + async (req, res) => { + try { + const { serverName } = req.params; + const user = createSafeUser(req.user); - if (!user.id) { - return res.status(401).json({ error: 'User not authenticated' }); - } + if (!user.id) { + return res.status(401).json({ error: 'User not authenticated' }); + } - logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`); + logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`); - const mcpManager = getMCPManager(); - const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id); - if (!serverConfig) { - return res.status(404).json({ - error: `MCP server '${serverName}' not found in configuration`, + const mcpManager = getMCPManager(); + const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id); + if (!serverConfig) { + return res.status(404).json({ + error: `MCP server '${serverName}' not found in configuration`, + }); + } + + await mcpManager.disconnectUserConnection(user.id, serverName); + logger.info( + `[MCP Reinitialize] Disconnected existing user connection for server: ${serverName}`, + ); + + /** @type {Record> | undefined} */ + let userMCPAuthMap; + if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { + userMCPAuthMap = await getUserMCPAuthMap({ + userId: user.id, + servers: [serverName], + findPluginAuthsByKeys, + }); + } + + const result = await reinitMCPServer({ + user, + serverName, + userMCPAuthMap, }); - } - await mcpManager.disconnectUserConnection(user.id, serverName); - logger.info( - `[MCP Reinitialize] Disconnected existing user connection for server: ${serverName}`, - ); + if (!result) { + return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' }); + } - /** @type {Record> | undefined} */ - let userMCPAuthMap; - if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { - userMCPAuthMap = await getUserMCPAuthMap({ - userId: user.id, - servers: [serverName], - findPluginAuthsByKeys, + const { success, message, oauthRequired, oauthUrl } = result; + + if (oauthRequired) { + const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName); + setOAuthCsrfCookie(res, flowId, OAUTH_CSRF_COOKIE_PATH); + } + + res.json({ + success, + message, + oauthUrl, + serverName, + oauthRequired, }); + } catch (error) { + logger.error('[MCP Reinitialize] Unexpected error', error); + res.status(500).json({ error: 'Internal server error' }); } - - const result = await reinitMCPServer({ - user, - serverName, - userMCPAuthMap, - }); - - if (!result) { - return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' }); - } - - const { success, message, oauthRequired, oauthUrl } = result; - - if (oauthRequired) { - const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName); - setOAuthCsrfCookie(res, flowId, OAUTH_CSRF_COOKIE_PATH); - } - - res.json({ - success, - message, - oauthUrl, - serverName, - oauthRequired, - }); - } catch (error) { - logger.error('[MCP Reinitialize] Unexpected error', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); + }, +); /** * Get connection status for all MCP servers @@ -639,7 +657,7 @@ router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) => * Check which authentication values exist for a specific MCP server * This endpoint returns only boolean flags indicating if values are set, not the actual values */ -router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => { +router.get('/:serverName/auth-values', requireJwtAuth, checkMCPUsePermissions, async (req, res) => { try { const { serverName } = req.params; const user = req.user; @@ -696,19 +714,6 @@ async function getOAuthHeaders(serverName, userId) { MCP Server CRUD Routes (User-Managed MCP Servers) */ -// Permission checkers for MCP server management -const checkMCPUsePermissions = generateCheckAccess({ - permissionType: PermissionTypes.MCP_SERVERS, - permissions: [Permissions.USE], - getRoleByName, -}); - -const checkMCPCreate = generateCheckAccess({ - permissionType: PermissionTypes.MCP_SERVERS, - permissions: [Permissions.USE, Permissions.CREATE], - getRoleByName, -}); - /** * Get list of accessible MCP servers * @route GET /api/mcp/servers diff --git a/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts b/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts index 9bad4dcfac..f1558db795 100644 --- a/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts +++ b/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts @@ -32,14 +32,22 @@ describe('LeaderElection with Redis', () => { process.setMaxListeners(200); }); - afterEach(async () => { - await Promise.all(instances.map((instance) => instance.resign())); - instances = []; - - // Clean up: clear the leader key directly from Redis + beforeEach(async () => { if (keyvRedisClient) { await keyvRedisClient.del(LeaderElection.LEADER_KEY); } + new LeaderElection().clearRefreshTimer(); + }); + + afterEach(async () => { + try { + await Promise.all(instances.map((instance) => instance.resign())); + } finally { + instances = []; + if (keyvRedisClient) { + await keyvRedisClient.del(LeaderElection.LEADER_KEY); + } + } }); afterAll(async () => { diff --git a/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts index 8437177c86..f73a5ed3e8 100644 --- a/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts +++ b/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts @@ -24,6 +24,13 @@ jest.mock('@librechat/data-schemas', () => ({ decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), })); +/** Bypass SSRF validation — these tests use real local HTTP servers. */ +jest.mock('~/auth', () => ({ + ...jest.requireActual('~/auth'), + isSSRFTarget: jest.fn(() => false), + resolveHostnameSSRF: jest.fn(async () => false), +})); + describe('MCP OAuth Flow — Real HTTP Server', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts new file mode 100644 index 0000000000..a5188e24b0 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts @@ -0,0 +1,228 @@ +/** + * Tests verifying MCP OAuth security hardening: + * + * 1. SSRF via OAuth URLs — validates that the OAuth handler rejects + * token_url, authorization_url, and revocation_endpoint values + * pointing to private/internal addresses. + * + * 2. redirect_uri manipulation — validates that user-supplied redirect_uri + * is ignored in favor of the server-controlled default. + */ + +import * as http from 'http'; +import * as net from 'net'; +import { TokenExchangeMethodEnum } from 'librechat-data-provider'; +import type { Socket } from 'net'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import { createOAuthMCPServer } from './helpers/oauthTestServer'; +import { MCPOAuthHandler } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +/** + * Mock only the DNS-dependent resolveHostnameSSRF; keep isSSRFTarget real. + * SSRF tests use literal private IPs (127.0.0.1, 169.254.169.254, 10.0.0.1) + * which are caught by isSSRFTarget before resolveHostnameSSRF is reached. + * This avoids non-deterministic DNS lookups in test execution. + */ +jest.mock('~/auth', () => ({ + ...jest.requireActual('~/auth'), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +describe('MCP OAuth SSRF protection', () => { + let oauthServer: OAuthTestServer; + let ssrfTargetServer: http.Server; + let ssrfTargetPort: number; + let ssrfRequestReceived: boolean; + let destroySSRFSockets: () => Promise; + + beforeEach(async () => { + ssrfRequestReceived = false; + + oauthServer = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + + ssrfTargetPort = await getFreePort(); + ssrfTargetServer = http.createServer((_req, res) => { + ssrfRequestReceived = true; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + access_token: 'ssrf-token', + token_type: 'Bearer', + expires_in: 3600, + }), + ); + }); + destroySSRFSockets = trackSockets(ssrfTargetServer); + await new Promise((resolve) => + ssrfTargetServer.listen(ssrfTargetPort, '127.0.0.1', resolve), + ); + }); + + afterEach(async () => { + try { + await oauthServer.close(); + } finally { + await destroySSRFSockets(); + } + }); + + it('should reject token_url pointing to a private IP (refreshOAuthTokens)', async () => { + const code = await oauthServer.getAuthCode(); + const tokenRes = await fetch(`${oauthServer.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + refresh_token: string; + }; + + const regRes = await fetch(`${oauthServer.url}register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ redirect_uris: ['http://localhost/callback'] }), + }); + const clientInfo = (await regRes.json()) as { + client_id: string; + client_secret: string; + }; + + const ssrfTokenUrl = `http://127.0.0.1:${ssrfTargetPort}/latest/meta-data/iam/security-credentials/`; + + await expect( + MCPOAuthHandler.refreshOAuthTokens( + initial.refresh_token, + { + serverName: 'ssrf-test-server', + serverUrl: oauthServer.url, + clientInfo: { + ...clientInfo, + redirect_uris: ['http://localhost/callback'], + }, + }, + {}, + { + token_url: ssrfTokenUrl, + client_id: clientInfo.client_id, + client_secret: clientInfo.client_secret, + token_exchange_method: TokenExchangeMethodEnum.DefaultPost, + }, + ), + ).rejects.toThrow(/targets a blocked address/); + + expect(ssrfRequestReceived).toBe(false); + }); + + it('should reject private authorization_url in initiateOAuthFlow', async () => { + await expect( + MCPOAuthHandler.initiateOAuthFlow( + 'test-server', + 'https://mcp.example.com/', + 'user-1', + {}, + { + authorization_url: 'http://169.254.169.254/authorize', + token_url: 'https://auth.example.com/token', + client_id: 'client', + client_secret: 'secret', + }, + ), + ).rejects.toThrow(/targets a blocked address/); + }); + + it('should reject private token_url in initiateOAuthFlow', async () => { + await expect( + MCPOAuthHandler.initiateOAuthFlow( + 'test-server', + 'https://mcp.example.com/', + 'user-1', + {}, + { + authorization_url: 'https://auth.example.com/authorize', + token_url: `http://127.0.0.1:${ssrfTargetPort}/token`, + client_id: 'client', + client_secret: 'secret', + }, + ), + ).rejects.toThrow(/targets a blocked address/); + + expect(ssrfRequestReceived).toBe(false); + }); + + it('should reject private revocationEndpoint in revokeOAuthToken', async () => { + await expect( + MCPOAuthHandler.revokeOAuthToken('test-server', 'some-token', 'access', { + serverUrl: 'https://mcp.example.com/', + clientId: 'client', + clientSecret: 'secret', + revocationEndpoint: 'http://10.0.0.1/revoke', + }), + ).rejects.toThrow(/targets a blocked address/); + }); +}); + +describe('MCP OAuth redirect_uri enforcement', () => { + it('should ignore attacker-supplied redirect_uri and use the server default', async () => { + const attackerRedirectUri = 'https://attacker.example.com/steal-code'; + + const result = await MCPOAuthHandler.initiateOAuthFlow( + 'victim-server', + 'https://mcp.example.com/', + 'victim-user-id', + {}, + { + authorization_url: 'https://auth.example.com/authorize', + token_url: 'https://auth.example.com/token', + client_id: 'attacker-client', + client_secret: 'attacker-secret', + redirect_uri: attackerRedirectUri, + }, + ); + + const authUrl = new URL(result.authorizationUrl); + const expectedRedirectUri = `${process.env.DOMAIN_SERVER || 'http://localhost:3080'}/api/mcp/victim-server/oauth/callback`; + expect(authUrl.searchParams.get('redirect_uri')).toBe(expectedRedirectUri); + expect(authUrl.searchParams.get('redirect_uri')).not.toBe(attackerRedirectUri); + }); +}); diff --git a/packages/api/src/mcp/__tests__/utils.test.ts b/packages/api/src/mcp/__tests__/utils.test.ts index 716a230ebe..e4fb31bdad 100644 --- a/packages/api/src/mcp/__tests__/utils.test.ts +++ b/packages/api/src/mcp/__tests__/utils.test.ts @@ -1,4 +1,5 @@ -import { normalizeServerName } from '../utils'; +import { normalizeServerName, redactServerSecrets, redactAllServerSecrets } from '~/mcp/utils'; +import type { ParsedServerConfig } from '~/mcp/types'; describe('normalizeServerName', () => { it('should not modify server names that already match the pattern', () => { @@ -26,3 +27,201 @@ describe('normalizeServerName', () => { expect(result).toMatch(/^[a-zA-Z0-9_.-]+$/); }); }); + +describe('redactServerSecrets', () => { + it('should strip apiKey.key from admin-sourced keys', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { + source: 'admin', + authorization_type: 'bearer', + key: 'super-secret-api-key', + }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.apiKey?.key).toBeUndefined(); + expect(redacted.apiKey?.source).toBe('admin'); + expect(redacted.apiKey?.authorization_type).toBe('bearer'); + }); + + it('should strip oauth.client_secret', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + oauth: { + client_id: 'my-client', + client_secret: 'super-secret-oauth', + scope: 'read', + }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.oauth?.client_secret).toBeUndefined(); + expect(redacted.oauth?.client_id).toBe('my-client'); + expect(redacted.oauth?.scope).toBe('read'); + }); + + it('should strip both apiKey.key and oauth.client_secret simultaneously', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { + source: 'admin', + authorization_type: 'custom', + custom_header: 'X-API-Key', + key: 'secret-key', + }, + oauth: { + client_id: 'cid', + client_secret: 'csecret', + }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.apiKey?.key).toBeUndefined(); + expect(redacted.apiKey?.custom_header).toBe('X-API-Key'); + expect(redacted.oauth?.client_secret).toBeUndefined(); + expect(redacted.oauth?.client_id).toBe('cid'); + }); + + it('should exclude headers from SSE configs', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'SSE Server', + }; + (config as ParsedServerConfig & { headers: Record }).headers = { + Authorization: 'Bearer admin-token-123', + 'X-Custom': 'safe-value', + }; + const redacted = redactServerSecrets(config); + expect((redacted as Record).headers).toBeUndefined(); + expect(redacted.title).toBe('SSE Server'); + }); + + it('should exclude env from stdio configs', () => { + const config: ParsedServerConfig = { + type: 'stdio', + command: 'node', + args: ['server.js'], + env: { DATABASE_URL: 'postgres://admin:password@localhost/db', PATH: '/usr/bin' }, + }; + const redacted = redactServerSecrets(config); + expect((redacted as Record).env).toBeUndefined(); + expect((redacted as Record).command).toBeUndefined(); + expect((redacted as Record).args).toBeUndefined(); + }); + + it('should exclude oauth_headers', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + oauth_headers: { Authorization: 'Bearer oauth-admin-token' }, + }; + const redacted = redactServerSecrets(config); + expect((redacted as Record).oauth_headers).toBeUndefined(); + }); + + it('should strip apiKey.key even for user-sourced keys', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { source: 'user', authorization_type: 'bearer', key: 'my-own-key' }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.apiKey?.key).toBeUndefined(); + expect(redacted.apiKey?.source).toBe('user'); + }); + + it('should not mutate the original config', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'secret' }, + oauth: { client_id: 'cid', client_secret: 'csecret' }, + }; + redactServerSecrets(config); + expect(config.apiKey?.key).toBe('secret'); + expect(config.oauth?.client_secret).toBe('csecret'); + }); + + it('should preserve all safe metadata fields', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'My Server', + description: 'A test server', + iconPath: '/icons/test.png', + chatMenu: true, + requiresOAuth: false, + capabilities: '{"tools":{}}', + tools: 'tool_a, tool_b', + dbId: 'abc123', + updatedAt: 1700000000000, + consumeOnly: false, + inspectionFailed: false, + customUserVars: { API_KEY: { title: 'API Key', description: 'Your key' } }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.title).toBe('My Server'); + expect(redacted.description).toBe('A test server'); + expect(redacted.iconPath).toBe('/icons/test.png'); + expect(redacted.chatMenu).toBe(true); + expect(redacted.requiresOAuth).toBe(false); + expect(redacted.capabilities).toBe('{"tools":{}}'); + expect(redacted.tools).toBe('tool_a, tool_b'); + expect(redacted.dbId).toBe('abc123'); + expect(redacted.updatedAt).toBe(1700000000000); + expect(redacted.consumeOnly).toBe(false); + expect(redacted.inspectionFailed).toBe(false); + expect(redacted.customUserVars).toEqual(config.customUserVars); + }); + + it('should pass URLs through unchanged', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://mcp.example.com/sse?param=value', + }; + const redacted = redactServerSecrets(config); + expect(redacted.url).toBe('https://mcp.example.com/sse?param=value'); + }); + + it('should only include explicitly allowlisted fields', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'Test', + }; + (config as Record).someNewSensitiveField = 'leaked-value'; + const redacted = redactServerSecrets(config); + expect((redacted as Record).someNewSensitiveField).toBeUndefined(); + expect(redacted.title).toBe('Test'); + }); +}); + +describe('redactAllServerSecrets', () => { + it('should redact secrets from all configs in the map', () => { + const configs: Record = { + 'server-a': { + type: 'sse', + url: 'https://a.com/mcp', + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'key-a' }, + }, + 'server-b': { + type: 'sse', + url: 'https://b.com/mcp', + oauth: { client_id: 'cid-b', client_secret: 'secret-b' }, + }, + 'server-c': { + type: 'stdio', + command: 'node', + args: ['c.js'], + }, + }; + const redacted = redactAllServerSecrets(configs); + expect(redacted['server-a'].apiKey?.key).toBeUndefined(); + expect(redacted['server-a'].apiKey?.source).toBe('admin'); + expect(redacted['server-b'].oauth?.client_secret).toBeUndefined(); + expect(redacted['server-b'].oauth?.client_id).toBe('cid-b'); + expect((redacted['server-c'] as Record).command).toBeUndefined(); + }); +}); diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 366d0d2fde..8d863bfe79 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -24,6 +24,7 @@ import { selectRegistrationAuthMethod, inferClientAuthMethod, } from './methods'; +import { isSSRFTarget, resolveHostnameSSRF } from '~/auth'; import { sanitizeUrlForLogging } from '~/mcp/utils'; /** Type for the OAuth metadata from the SDK */ @@ -144,7 +145,9 @@ export class MCPOAuthHandler { resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {}, fetchFn); if (resourceMetadata?.authorization_servers?.length) { - authServerUrl = new URL(resourceMetadata.authorization_servers[0]); + const discoveredAuthServer = resourceMetadata.authorization_servers[0]; + await this.validateOAuthUrl(discoveredAuthServer, 'authorization_server'); + authServerUrl = new URL(discoveredAuthServer); logger.debug( `[MCPOAuth] Found authorization server from resource metadata: ${authServerUrl}`, ); @@ -200,6 +203,19 @@ export class MCPOAuthHandler { logger.debug(`[MCPOAuth] OAuth metadata discovered successfully`); const metadata = await OAuthMetadataSchema.parseAsync(rawMetadata); + const endpointChecks: Promise[] = []; + if (metadata.registration_endpoint) { + endpointChecks.push( + this.validateOAuthUrl(metadata.registration_endpoint, 'registration_endpoint'), + ); + } + if (metadata.token_endpoint) { + endpointChecks.push(this.validateOAuthUrl(metadata.token_endpoint, 'token_endpoint')); + } + if (endpointChecks.length > 0) { + await Promise.all(endpointChecks); + } + logger.debug(`[MCPOAuth] OAuth metadata parsed successfully`); return { metadata: metadata as unknown as OAuthMetadata, @@ -355,10 +371,14 @@ export class MCPOAuthHandler { logger.debug(`[MCPOAuth] Generated flowId: ${flowId}, state: ${state}`); try { - // Check if we have pre-configured OAuth settings if (config?.authorization_url && config?.token_url && config?.client_id) { logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for ${serverName}`); + await Promise.all([ + this.validateOAuthUrl(config.authorization_url, 'authorization_url'), + this.validateOAuthUrl(config.token_url, 'token_url'), + ]); + const skipCodeChallengeCheck = config?.skip_code_challenge_check === true || process.env.MCP_SKIP_CODE_CHALLENGE_CHECK === 'true'; @@ -410,10 +430,11 @@ export class MCPOAuthHandler { code_challenge_methods_supported: codeChallengeMethodsSupported, }; logger.debug(`[MCPOAuth] metadata for "${serverName}": ${JSON.stringify(metadata)}`); + const redirectUri = this.getDefaultRedirectUri(serverName); const clientInfo: OAuthClientInformation = { client_id: config.client_id, client_secret: config.client_secret, - redirect_uris: [config.redirect_uri || this.getDefaultRedirectUri(serverName)], + redirect_uris: [redirectUri], scope: config.scope, token_endpoint_auth_method: tokenEndpointAuthMethod, }; @@ -422,7 +443,7 @@ export class MCPOAuthHandler { const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, { metadata: metadata as unknown as SDKOAuthMetadata, clientInformation: clientInfo, - redirectUrl: clientInfo.redirect_uris?.[0] || this.getDefaultRedirectUri(serverName), + redirectUrl: redirectUri, scope: config.scope, }); @@ -462,8 +483,7 @@ export class MCPOAuthHandler { `[MCPOAuth] OAuth metadata discovered, auth server URL: ${sanitizeUrlForLogging(authServerUrl)}`, ); - /** Dynamic client registration based on the discovered metadata */ - const redirectUri = config?.redirect_uri || this.getDefaultRedirectUri(serverName); + const redirectUri = this.getDefaultRedirectUri(serverName); logger.debug(`[MCPOAuth] Registering OAuth client with redirect URI: ${redirectUri}`); const clientInfo = await this.registerOAuthClient( @@ -672,6 +692,24 @@ export class MCPOAuthHandler { return randomBytes(32).toString('base64url'); } + /** Validates an OAuth URL is not targeting a private/internal address */ + private static async validateOAuthUrl(url: string, fieldName: string): Promise { + let hostname: string; + try { + hostname = new URL(url).hostname; + } catch { + throw new Error(`Invalid OAuth ${fieldName}: ${sanitizeUrlForLogging(url)}`); + } + + if (isSSRFTarget(hostname)) { + throw new Error(`OAuth ${fieldName} targets a blocked address`); + } + + if (await resolveHostnameSSRF(hostname)) { + throw new Error(`OAuth ${fieldName} resolves to a private IP address`); + } + } + private static readonly STATE_MAP_TYPE = 'mcp_oauth_state'; /** @@ -783,10 +821,10 @@ export class MCPOAuthHandler { scope: metadata.clientInfo.scope, }); - /** Use the stored client information and metadata to determine the token URL */ let tokenUrl: string; let authMethods: string[] | undefined; if (config?.token_url) { + await this.validateOAuthUrl(config.token_url, 'token_url'); tokenUrl = config.token_url; authMethods = config.token_endpoint_auth_methods_supported; } else if (!metadata.serverUrl) { @@ -813,6 +851,7 @@ export class MCPOAuthHandler { tokenUrl = oauthMetadata.token_endpoint; authMethods = oauthMetadata.token_endpoint_auth_methods_supported; } + await this.validateOAuthUrl(tokenUrl, 'token_url'); } const body = new URLSearchParams({ @@ -886,10 +925,10 @@ export class MCPOAuthHandler { return this.processRefreshResponse(tokens, metadata.serverName, 'stored client info'); } - // Fallback: If we have pre-configured OAuth settings, use them if (config?.token_url && config?.client_id) { logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for token refresh`); + await this.validateOAuthUrl(config.token_url, 'token_url'); const tokenUrl = new URL(config.token_url); const body = new URLSearchParams({ @@ -987,6 +1026,7 @@ export class MCPOAuthHandler { } else { tokenUrl = new URL(oauthMetadata.token_endpoint); } + await this.validateOAuthUrl(tokenUrl.href, 'token_url'); const body = new URLSearchParams({ grant_type: 'refresh_token', @@ -1036,7 +1076,9 @@ export class MCPOAuthHandler { }, oauthHeaders: Record = {}, ): Promise { - // build the revoke URL, falling back to the server URL + /revoke if no revocation endpoint is provided + if (metadata.revocationEndpoint != null) { + await this.validateOAuthUrl(metadata.revocationEndpoint, 'revocation_endpoint'); + } const revokeUrl: URL = metadata.revocationEndpoint != null ? new URL(metadata.revocationEndpoint) diff --git a/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts b/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts index 1c755ae0f0..38ed51cd99 100644 --- a/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts +++ b/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts @@ -1456,4 +1456,102 @@ describe('ServerConfigsDB', () => { expect(retrieved?.apiKey?.key).toBeUndefined(); }); }); + + describe('DB layer returns decrypted secrets (redaction is at controller layer)', () => { + it('should return decrypted apiKey.key to VIEW-only user via get()', async () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'Secret API Key Server', + apiKey: { + source: 'admin', + authorization_type: 'bearer', + key: 'admin-secret-api-key', + }, + }; + const created = await serverConfigsDB.add('temp-name', config, userId); + + const role = await mongoose.models.AccessRole.findOne({ + accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, + }); + await mongoose.models.AclEntry.create({ + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: new mongoose.Types.ObjectId(userId2), + resourceType: ResourceType.MCPSERVER, + resourceId: new mongoose.Types.ObjectId(created.config.dbId!), + permBits: PermissionBits.VIEW, + roleId: role!._id, + grantedBy: new mongoose.Types.ObjectId(userId), + }); + + const result = await serverConfigsDB.get(created.serverName, userId2); + expect(result).toBeDefined(); + expect(result?.apiKey?.key).toBe('admin-secret-api-key'); + }); + + it('should return decrypted oauth.client_secret to VIEW-only user via get()', async () => { + const config = createSSEConfig('Secret OAuth Server', 'Test', { + client_id: 'my-client-id', + client_secret: 'admin-oauth-secret', + }); + const created = await serverConfigsDB.add('temp-name', config, userId); + + const role = await mongoose.models.AccessRole.findOne({ + accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, + }); + await mongoose.models.AclEntry.create({ + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: new mongoose.Types.ObjectId(userId2), + resourceType: ResourceType.MCPSERVER, + resourceId: new mongoose.Types.ObjectId(created.config.dbId!), + permBits: PermissionBits.VIEW, + roleId: role!._id, + grantedBy: new mongoose.Types.ObjectId(userId), + }); + + const result = await serverConfigsDB.get(created.serverName, userId2); + expect(result).toBeDefined(); + expect(result?.oauth?.client_secret).toBe('admin-oauth-secret'); + }); + + it('should return decrypted secrets to VIEW-only user via getAll()', async () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'Shared Secret Server', + apiKey: { + source: 'admin', + authorization_type: 'bearer', + key: 'shared-api-key', + }, + oauth: { + client_id: 'shared-client', + client_secret: 'shared-oauth-secret', + }, + }; + const created = await serverConfigsDB.add('temp-name', config, userId); + + const role = await mongoose.models.AccessRole.findOne({ + accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, + }); + await mongoose.models.AclEntry.create({ + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: new mongoose.Types.ObjectId(userId2), + resourceType: ResourceType.MCPSERVER, + resourceId: new mongoose.Types.ObjectId(created.config.dbId!), + permBits: PermissionBits.VIEW, + roleId: role!._id, + grantedBy: new mongoose.Types.ObjectId(userId), + }); + + const result = await serverConfigsDB.getAll(userId2); + const serverConfig = result[created.serverName]; + expect(serverConfig).toBeDefined(); + expect(serverConfig?.apiKey?.key).toBe('shared-api-key'); + expect(serverConfig?.oauth?.client_secret).toBe('shared-oauth-secret'); + }); + }); }); diff --git a/packages/api/src/mcp/utils.ts b/packages/api/src/mcp/utils.ts index fddebb9db3..c517388a76 100644 --- a/packages/api/src/mcp/utils.ts +++ b/packages/api/src/mcp/utils.ts @@ -1,6 +1,66 @@ import { Constants } from 'librechat-data-provider'; +import type { ParsedServerConfig } from '~/mcp/types'; export const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`); + +/** + * Allowlist-based sanitization for API responses. Only explicitly listed fields are included; + * new fields added to ParsedServerConfig are excluded by default until allowlisted here. + * + * URLs are returned as-is: DB-stored configs reject ${VAR} patterns at validation time + * (MCPServerUserInputSchema), and YAML configs are admin-managed. Env variable resolution + * is handled at the schema/input boundary, not the output boundary. + */ +export function redactServerSecrets(config: ParsedServerConfig): Partial { + const safe: Partial = { + type: config.type, + url: config.url, + title: config.title, + description: config.description, + iconPath: config.iconPath, + chatMenu: config.chatMenu, + requiresOAuth: config.requiresOAuth, + capabilities: config.capabilities, + tools: config.tools, + toolFunctions: config.toolFunctions, + initDuration: config.initDuration, + updatedAt: config.updatedAt, + dbId: config.dbId, + consumeOnly: config.consumeOnly, + inspectionFailed: config.inspectionFailed, + customUserVars: config.customUserVars, + serverInstructions: config.serverInstructions, + }; + + if (config.apiKey) { + safe.apiKey = { + source: config.apiKey.source, + authorization_type: config.apiKey.authorization_type, + ...(config.apiKey.custom_header && { custom_header: config.apiKey.custom_header }), + }; + } + + if (config.oauth) { + const { client_secret: _secret, ...safeOAuth } = config.oauth; + safe.oauth = safeOAuth; + } + + return Object.fromEntries( + Object.entries(safe).filter(([, v]) => v !== undefined), + ) as Partial; +} + +/** Applies allowlist-based sanitization to a map of server configs. */ +export function redactAllServerSecrets( + configs: Record, +): Record> { + const result: Record> = {}; + for (const [key, config] of Object.entries(configs)) { + result[key] = redactServerSecrets(config); + } + return result; +} + /** * Normalizes a server name to match the pattern ^[a-zA-Z0-9_.-]+$ * This is required for Azure OpenAI models with Tool Calling From ca79a03135cde32eb112b4292ab02f80e83bcc69 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 13 Mar 2026 23:40:44 -0400 Subject: [PATCH 025/111] =?UTF-8?q?=F0=9F=9A=A6=20fix:=20Add=20Rate=20Limi?= =?UTF-8?q?ting=20to=20Conversation=20Duplicate=20Endpoint=20(#12218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add rate limiting to conversation duplicate endpoint * chore: linter * fix: address review findings for conversation duplicate rate limiting * refactor: streamline test mocks for conversation routes - Consolidated mock implementations into a dedicated `convos-route-mocks.js` file to enhance maintainability and readability of test files. - Updated tests in `convos-duplicate-ratelimit.spec.js` and `convos.spec.js` to utilize the new mock structure, improving clarity and reducing redundancy. - Enhanced the `duplicateConversation` function to accept an optional title parameter for better flexibility in conversation duplication. * chore: rename files --- .../middleware/limiters/forkLimiters.js | 2 +- .../__test-utils__/convos-route-mocks.js | 92 ++++++++++++ .../convos-duplicate-ratelimit.spec.js | 135 ++++++++++++++++++ api/server/routes/__tests__/convos.spec.js | 119 +++------------ api/server/routes/convos.js | 3 +- api/server/utils/import/fork.js | 14 +- 6 files changed, 252 insertions(+), 113 deletions(-) create mode 100644 api/server/routes/__test-utils__/convos-route-mocks.js create mode 100644 api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js diff --git a/api/server/middleware/limiters/forkLimiters.js b/api/server/middleware/limiters/forkLimiters.js index e0aa65700c..f1e9b15f11 100644 --- a/api/server/middleware/limiters/forkLimiters.js +++ b/api/server/middleware/limiters/forkLimiters.js @@ -48,7 +48,7 @@ const createForkHandler = (ip = true) => { }; await logViolation(req, res, type, errorMessage, forkViolationScore); - res.status(429).json({ message: 'Too many conversation fork requests. Try again later' }); + res.status(429).json({ message: 'Too many requests. Try again later' }); }; }; diff --git a/api/server/routes/__test-utils__/convos-route-mocks.js b/api/server/routes/__test-utils__/convos-route-mocks.js new file mode 100644 index 0000000000..ca5bafeda9 --- /dev/null +++ b/api/server/routes/__test-utils__/convos-route-mocks.js @@ -0,0 +1,92 @@ +module.exports = { + agents: () => ({ sleep: jest.fn() }), + + api: (overrides = {}) => ({ + isEnabled: jest.fn(), + createAxiosInstance: jest.fn(() => ({ + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + })), + logAxiosError: jest.fn(), + ...overrides, + }), + + dataSchemas: () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + createModels: jest.fn(() => ({ + User: {}, + Conversation: {}, + Message: {}, + SharedLink: {}, + })), + }), + + dataProvider: (overrides = {}) => ({ + CacheKeys: { GEN_TITLE: 'GEN_TITLE' }, + EModelEndpoint: { + azureAssistants: 'azureAssistants', + assistants: 'assistants', + }, + ...overrides, + }), + + conversationModel: () => ({ + getConvosByCursor: jest.fn(), + getConvo: jest.fn(), + deleteConvos: jest.fn(), + saveConvo: jest.fn(), + }), + + toolCallModel: () => ({ deleteToolCalls: jest.fn() }), + + sharedModels: () => ({ + deleteAllSharedLinks: jest.fn(), + deleteConvoSharedLink: jest.fn(), + }), + + requireJwtAuth: () => (req, res, next) => next(), + + middlewarePassthrough: () => ({ + createImportLimiters: jest.fn(() => ({ + importIpLimiter: (req, res, next) => next(), + importUserLimiter: (req, res, next) => next(), + })), + createForkLimiters: jest.fn(() => ({ + forkIpLimiter: (req, res, next) => next(), + forkUserLimiter: (req, res, next) => next(), + })), + configMiddleware: (req, res, next) => next(), + validateConvoAccess: (req, res, next) => next(), + }), + + forkUtils: () => ({ + forkConversation: jest.fn(), + duplicateConversation: jest.fn(), + }), + + importUtils: () => ({ importConversations: jest.fn() }), + + logStores: () => jest.fn(), + + multerSetup: () => ({ + storage: {}, + importFileFilter: jest.fn(), + }), + + multerLib: () => + jest.fn(() => ({ + single: jest.fn(() => (req, res, next) => { + req.file = { path: '/tmp/test-file.json' }; + next(); + }), + })), + + assistantEndpoint: () => ({ initializeClient: jest.fn() }), +}; diff --git a/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js b/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js new file mode 100644 index 0000000000..788119a569 --- /dev/null +++ b/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js @@ -0,0 +1,135 @@ +const express = require('express'); +const request = require('supertest'); + +const MOCKS = '../__test-utils__/convos-route-mocks'; + +jest.mock('@librechat/agents', () => require(MOCKS).agents()); +jest.mock('@librechat/api', () => require(MOCKS).api({ limiterCache: jest.fn(() => undefined) })); +jest.mock('@librechat/data-schemas', () => require(MOCKS).dataSchemas()); +jest.mock('librechat-data-provider', () => + require(MOCKS).dataProvider({ ViolationTypes: { FILE_UPLOAD_LIMIT: 'file_upload_limit' } }), +); + +jest.mock('~/cache/logViolation', () => jest.fn().mockResolvedValue(undefined)); +jest.mock('~/cache/getLogStores', () => require(MOCKS).logStores()); +jest.mock('~/models/Conversation', () => require(MOCKS).conversationModel()); +jest.mock('~/models/ToolCall', () => require(MOCKS).toolCallModel()); +jest.mock('~/models', () => require(MOCKS).sharedModels()); +jest.mock('~/server/middleware/requireJwtAuth', () => require(MOCKS).requireJwtAuth()); + +jest.mock('~/server/middleware', () => { + const { createForkLimiters } = jest.requireActual('~/server/middleware/limiters/forkLimiters'); + return { + createImportLimiters: jest.fn(() => ({ + importIpLimiter: (req, res, next) => next(), + importUserLimiter: (req, res, next) => next(), + })), + createForkLimiters, + configMiddleware: (req, res, next) => next(), + validateConvoAccess: (req, res, next) => next(), + }; +}); + +jest.mock('~/server/utils/import/fork', () => require(MOCKS).forkUtils()); +jest.mock('~/server/utils/import', () => require(MOCKS).importUtils()); +jest.mock('~/server/routes/files/multer', () => require(MOCKS).multerSetup()); +jest.mock('multer', () => require(MOCKS).multerLib()); +jest.mock('~/server/services/Endpoints/azureAssistants', () => require(MOCKS).assistantEndpoint()); +jest.mock('~/server/services/Endpoints/assistants', () => require(MOCKS).assistantEndpoint()); + +describe('POST /api/convos/duplicate - Rate Limiting', () => { + let app; + let duplicateConversation; + const savedEnv = {}; + + beforeAll(() => { + savedEnv.FORK_USER_MAX = process.env.FORK_USER_MAX; + savedEnv.FORK_USER_WINDOW = process.env.FORK_USER_WINDOW; + savedEnv.FORK_IP_MAX = process.env.FORK_IP_MAX; + savedEnv.FORK_IP_WINDOW = process.env.FORK_IP_WINDOW; + }); + + afterAll(() => { + for (const key of Object.keys(savedEnv)) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + }); + + const setupApp = () => { + jest.clearAllMocks(); + jest.isolateModules(() => { + const convosRouter = require('../convos'); + ({ duplicateConversation } = require('~/server/utils/import/fork')); + + app = express(); + app.use(express.json()); + app.use((req, res, next) => { + req.user = { id: 'rate-limit-test-user' }; + next(); + }); + app.use('/api/convos', convosRouter); + }); + + duplicateConversation.mockResolvedValue({ + conversation: { conversationId: 'duplicated-conv' }, + }); + }; + + describe('user limit', () => { + beforeEach(() => { + process.env.FORK_USER_MAX = '2'; + process.env.FORK_USER_WINDOW = '1'; + process.env.FORK_IP_MAX = '100'; + process.env.FORK_IP_WINDOW = '1'; + setupApp(); + }); + + it('should return 429 after exceeding the user rate limit', async () => { + const userMax = parseInt(process.env.FORK_USER_MAX, 10); + + for (let i = 0; i < userMax; i++) { + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(201); + } + + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(429); + expect(res.body.message).toMatch(/too many/i); + }); + }); + + describe('IP limit', () => { + beforeEach(() => { + process.env.FORK_USER_MAX = '100'; + process.env.FORK_USER_WINDOW = '1'; + process.env.FORK_IP_MAX = '2'; + process.env.FORK_IP_WINDOW = '1'; + setupApp(); + }); + + it('should return 429 after exceeding the IP rate limit', async () => { + const ipMax = parseInt(process.env.FORK_IP_MAX, 10); + + for (let i = 0; i < ipMax; i++) { + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(201); + } + + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(429); + expect(res.body.message).toMatch(/too many/i); + }); + }); +}); diff --git a/api/server/routes/__tests__/convos.spec.js b/api/server/routes/__tests__/convos.spec.js index 931ef006d0..3bdeac32db 100644 --- a/api/server/routes/__tests__/convos.spec.js +++ b/api/server/routes/__tests__/convos.spec.js @@ -1,109 +1,24 @@ const express = require('express'); const request = require('supertest'); -jest.mock('@librechat/agents', () => ({ - sleep: jest.fn(), -})); +const MOCKS = '../__test-utils__/convos-route-mocks'; -jest.mock('@librechat/api', () => ({ - isEnabled: jest.fn(), - createAxiosInstance: jest.fn(() => ({ - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - delete: jest.fn(), - })), - logAxiosError: jest.fn(), -})); - -jest.mock('@librechat/data-schemas', () => ({ - logger: { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, - createModels: jest.fn(() => ({ - User: {}, - Conversation: {}, - Message: {}, - SharedLink: {}, - })), -})); - -jest.mock('~/models/Conversation', () => ({ - getConvosByCursor: jest.fn(), - getConvo: jest.fn(), - deleteConvos: jest.fn(), - saveConvo: jest.fn(), -})); - -jest.mock('~/models/ToolCall', () => ({ - deleteToolCalls: jest.fn(), -})); - -jest.mock('~/models', () => ({ - deleteAllSharedLinks: jest.fn(), - deleteConvoSharedLink: jest.fn(), -})); - -jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next()); - -jest.mock('~/server/middleware', () => ({ - createImportLimiters: jest.fn(() => ({ - importIpLimiter: (req, res, next) => next(), - importUserLimiter: (req, res, next) => next(), - })), - createForkLimiters: jest.fn(() => ({ - forkIpLimiter: (req, res, next) => next(), - forkUserLimiter: (req, res, next) => next(), - })), - configMiddleware: (req, res, next) => next(), - validateConvoAccess: (req, res, next) => next(), -})); - -jest.mock('~/server/utils/import/fork', () => ({ - forkConversation: jest.fn(), - duplicateConversation: jest.fn(), -})); - -jest.mock('~/server/utils/import', () => ({ - importConversations: jest.fn(), -})); - -jest.mock('~/cache/getLogStores', () => jest.fn()); - -jest.mock('~/server/routes/files/multer', () => ({ - storage: {}, - importFileFilter: jest.fn(), -})); - -jest.mock('multer', () => { - return jest.fn(() => ({ - single: jest.fn(() => (req, res, next) => { - req.file = { path: '/tmp/test-file.json' }; - next(); - }), - })); -}); - -jest.mock('librechat-data-provider', () => ({ - CacheKeys: { - GEN_TITLE: 'GEN_TITLE', - }, - EModelEndpoint: { - azureAssistants: 'azureAssistants', - assistants: 'assistants', - }, -})); - -jest.mock('~/server/services/Endpoints/azureAssistants', () => ({ - initializeClient: jest.fn(), -})); - -jest.mock('~/server/services/Endpoints/assistants', () => ({ - initializeClient: jest.fn(), -})); +jest.mock('@librechat/agents', () => require(MOCKS).agents()); +jest.mock('@librechat/api', () => require(MOCKS).api()); +jest.mock('@librechat/data-schemas', () => require(MOCKS).dataSchemas()); +jest.mock('librechat-data-provider', () => require(MOCKS).dataProvider()); +jest.mock('~/models/Conversation', () => require(MOCKS).conversationModel()); +jest.mock('~/models/ToolCall', () => require(MOCKS).toolCallModel()); +jest.mock('~/models', () => require(MOCKS).sharedModels()); +jest.mock('~/server/middleware/requireJwtAuth', () => require(MOCKS).requireJwtAuth()); +jest.mock('~/server/middleware', () => require(MOCKS).middlewarePassthrough()); +jest.mock('~/server/utils/import/fork', () => require(MOCKS).forkUtils()); +jest.mock('~/server/utils/import', () => require(MOCKS).importUtils()); +jest.mock('~/cache/getLogStores', () => require(MOCKS).logStores()); +jest.mock('~/server/routes/files/multer', () => require(MOCKS).multerSetup()); +jest.mock('multer', () => require(MOCKS).multerLib()); +jest.mock('~/server/services/Endpoints/azureAssistants', () => require(MOCKS).assistantEndpoint()); +jest.mock('~/server/services/Endpoints/assistants', () => require(MOCKS).assistantEndpoint()); describe('Convos Routes', () => { let app; diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index bb9c4ebea9..5f0c35fa0a 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -224,6 +224,7 @@ router.post('/update', validateConvoAccess, async (req, res) => { }); const { importIpLimiter, importUserLimiter } = createImportLimiters(); +/** Fork and duplicate share one rate-limit budget (same "clone" operation class) */ const { forkIpLimiter, forkUserLimiter } = createForkLimiters(); const upload = multer({ storage: storage, fileFilter: importFileFilter }); @@ -280,7 +281,7 @@ router.post('/fork', forkIpLimiter, forkUserLimiter, async (req, res) => { } }); -router.post('/duplicate', async (req, res) => { +router.post('/duplicate', forkIpLimiter, forkUserLimiter, async (req, res) => { const { conversationId, title } = req.body; try { diff --git a/api/server/utils/import/fork.js b/api/server/utils/import/fork.js index c4ce8cb5d4..f896de378c 100644 --- a/api/server/utils/import/fork.js +++ b/api/server/utils/import/fork.js @@ -358,16 +358,15 @@ function splitAtTargetLevel(messages, targetMessageId) { * @param {object} params - The parameters for duplicating the conversation. * @param {string} params.userId - The ID of the user duplicating the conversation. * @param {string} params.conversationId - The ID of the conversation to duplicate. + * @param {string} [params.title] - Optional title override for the duplicate. * @returns {Promise<{ conversation: TConversation, messages: TMessage[] }>} The duplicated conversation and messages. */ -async function duplicateConversation({ userId, conversationId }) { - // Get original conversation +async function duplicateConversation({ userId, conversationId, title }) { const originalConvo = await getConvo(userId, conversationId); if (!originalConvo) { throw new Error('Conversation not found'); } - // Get original messages const originalMessages = await getMessages({ user: userId, conversationId, @@ -383,14 +382,11 @@ async function duplicateConversation({ userId, conversationId }) { cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder); - const result = importBatchBuilder.finishConversation( - originalConvo.title, - new Date(), - originalConvo, - ); + const duplicateTitle = title || originalConvo.title; + const result = importBatchBuilder.finishConversation(duplicateTitle, new Date(), originalConvo); await importBatchBuilder.saveBatch(); logger.debug( - `user: ${userId} | New conversation "${originalConvo.title}" duplicated from conversation ID ${conversationId}`, + `user: ${userId} | New conversation "${duplicateTitle}" duplicated from conversation ID ${conversationId}`, ); const conversation = await getConvo(userId, result.conversation.conversationId); From 189cdf581d1e5f4f565894f6a9fd3b2704f4fd20 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 13 Mar 2026 23:42:37 -0400 Subject: [PATCH 026/111] =?UTF-8?q?=F0=9F=94=90=20fix:=20Add=20User=20Filt?= =?UTF-8?q?er=20to=20Message=20Deletion=20(#12220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add user filter to message deletion to prevent IDOR * refactor: streamline DELETE request syntax in messages-delete test - Simplified the DELETE request syntax in the messages-delete.spec.js test file by combining multiple lines into a single line for improved readability. This change enhances the clarity of the test code without altering its functionality. * fix: address review findings for message deletion IDOR fix * fix: add user filter to message deletion in conversation tests - Included a user filter in the message deletion test to ensure proper handling of user-specific deletions, enhancing the accuracy of the test case and preventing potential IDOR vulnerabilities. * chore: lint --- api/models/Conversation.js | 3 +- api/models/Conversation.spec.js | 1 + .../routes/__tests__/messages-delete.spec.js | 200 ++++++++++++++++++ api/server/routes/messages.js | 4 +- 4 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 api/server/routes/__tests__/messages-delete.spec.js diff --git a/api/models/Conversation.js b/api/models/Conversation.js index 32eac1a764..121eaa9696 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -228,7 +228,7 @@ module.exports = { }, ], }; - } catch (err) { + } catch (_err) { logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning'); } if (cursorFilter) { @@ -361,6 +361,7 @@ module.exports = { const deleteMessagesResult = await deleteMessages({ conversationId: { $in: conversationIds }, + user, }); return { ...deleteConvoResult, messages: deleteMessagesResult }; diff --git a/api/models/Conversation.spec.js b/api/models/Conversation.spec.js index bd415b4165..e9e4b5762d 100644 --- a/api/models/Conversation.spec.js +++ b/api/models/Conversation.spec.js @@ -549,6 +549,7 @@ describe('Conversation Operations', () => { expect(result.messages.deletedCount).toBe(5); expect(deleteMessages).toHaveBeenCalledWith({ conversationId: { $in: [mockConversationData.conversationId] }, + user: 'user123', }); // Verify conversation was deleted diff --git a/api/server/routes/__tests__/messages-delete.spec.js b/api/server/routes/__tests__/messages-delete.spec.js new file mode 100644 index 0000000000..e134eecfd0 --- /dev/null +++ b/api/server/routes/__tests__/messages-delete.spec.js @@ -0,0 +1,200 @@ +const mongoose = require('mongoose'); +const express = require('express'); +const request = require('supertest'); +const { v4: uuidv4 } = require('uuid'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.mock('@librechat/agents', () => ({ + sleep: jest.fn(), +})); + +jest.mock('@librechat/api', () => ({ + unescapeLaTeX: jest.fn((x) => x), + countTokens: jest.fn().mockResolvedValue(10), +})); + +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), +})); + +jest.mock('~/models', () => ({ + saveConvo: jest.fn(), + getMessage: jest.fn(), + saveMessage: jest.fn(), + getMessages: jest.fn(), + updateMessage: jest.fn(), + deleteMessages: jest.fn(), +})); + +jest.mock('~/server/services/Artifacts/update', () => ({ + findAllArtifacts: jest.fn(), + replaceArtifactContent: jest.fn(), +})); + +jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next()); + +jest.mock('~/server/middleware', () => ({ + requireJwtAuth: (req, res, next) => next(), + validateMessageReq: (req, res, next) => next(), +})); + +jest.mock('~/models/Conversation', () => ({ + getConvosQueried: jest.fn(), +})); + +jest.mock('~/db/models', () => ({ + Message: { + findOne: jest.fn(), + find: jest.fn(), + meiliSearch: jest.fn(), + }, +})); + +/* ─── Model-level tests: real MongoDB, proves cross-user deletion is prevented ─── */ + +const { messageSchema } = require('@librechat/data-schemas'); + +describe('deleteMessages – model-level IDOR prevention', () => { + let mongoServer; + let Message; + + const ownerUserId = 'user-owner-111'; + const attackerUserId = 'user-attacker-222'; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + Message = mongoose.models.Message || mongoose.model('Message', messageSchema); + await mongoose.connect(mongoServer.getUri()); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await Message.deleteMany({}); + }); + + it("should NOT delete another user's message when attacker supplies victim messageId", async () => { + const conversationId = uuidv4(); + const victimMsgId = 'victim-msg-001'; + + await Message.create({ + messageId: victimMsgId, + conversationId, + user: ownerUserId, + text: 'Sensitive owner data', + }); + + await Message.deleteMany({ messageId: victimMsgId, user: attackerUserId }); + + const victimMsg = await Message.findOne({ messageId: victimMsgId }).lean(); + expect(victimMsg).not.toBeNull(); + expect(victimMsg.user).toBe(ownerUserId); + expect(victimMsg.text).toBe('Sensitive owner data'); + }); + + it("should delete the user's own message", async () => { + const conversationId = uuidv4(); + const ownMsgId = 'own-msg-001'; + + await Message.create({ + messageId: ownMsgId, + conversationId, + user: ownerUserId, + text: 'My message', + }); + + const result = await Message.deleteMany({ messageId: ownMsgId, user: ownerUserId }); + expect(result.deletedCount).toBe(1); + + const deleted = await Message.findOne({ messageId: ownMsgId }).lean(); + expect(deleted).toBeNull(); + }); + + it('should scope deletion by conversationId, messageId, and user together', async () => { + const convoA = uuidv4(); + const convoB = uuidv4(); + + await Message.create([ + { messageId: 'msg-a1', conversationId: convoA, user: ownerUserId, text: 'A1' }, + { messageId: 'msg-b1', conversationId: convoB, user: ownerUserId, text: 'B1' }, + ]); + + await Message.deleteMany({ messageId: 'msg-a1', conversationId: convoA, user: attackerUserId }); + + const remaining = await Message.find({ user: ownerUserId }).lean(); + expect(remaining).toHaveLength(2); + }); +}); + +/* ─── Route-level tests: supertest + mocked deleteMessages ─── */ + +describe('DELETE /:conversationId/:messageId – route handler', () => { + let app; + const { deleteMessages } = require('~/models'); + + const authenticatedUserId = 'user-owner-123'; + + beforeAll(() => { + const messagesRouter = require('../messages'); + + app = express(); + app.use(express.json()); + app.use((req, res, next) => { + req.user = { id: authenticatedUserId }; + next(); + }); + app.use('/api/messages', messagesRouter); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should pass user and conversationId in the deleteMessages filter', async () => { + deleteMessages.mockResolvedValue({ deletedCount: 1 }); + + await request(app).delete('/api/messages/convo-1/msg-1'); + + expect(deleteMessages).toHaveBeenCalledTimes(1); + expect(deleteMessages).toHaveBeenCalledWith({ + messageId: 'msg-1', + conversationId: 'convo-1', + user: authenticatedUserId, + }); + }); + + it('should return 204 on successful deletion', async () => { + deleteMessages.mockResolvedValue({ deletedCount: 1 }); + + const response = await request(app).delete('/api/messages/convo-1/msg-owned'); + + expect(response.status).toBe(204); + expect(deleteMessages).toHaveBeenCalledWith({ + messageId: 'msg-owned', + conversationId: 'convo-1', + user: authenticatedUserId, + }); + }); + + it('should return 500 when deleteMessages throws', async () => { + deleteMessages.mockRejectedValue(new Error('DB failure')); + + const response = await request(app).delete('/api/messages/convo-1/msg-1'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Internal server error' }); + }); +}); diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index c208e9c406..03286bc7f1 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -404,8 +404,8 @@ router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (re router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { - const { messageId } = req.params; - await deleteMessages({ messageId }); + const { conversationId, messageId } = req.params; + await deleteMessages({ messageId, conversationId, user: req.user.id }); res.status(204).send(); } catch (error) { logger.error('Error deleting message:', error); From 71a3b48504785362f9e705726990126014200095 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 01:51:31 -0400 Subject: [PATCH 027/111] =?UTF-8?q?=F0=9F=94=91=20fix:=20Require=20OTP=20V?= =?UTF-8?q?erification=20for=202FA=20Re-Enrollment=20and=20Backup=20Code?= =?UTF-8?q?=20Regeneration=20(#12223)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: require OTP verification for 2FA re-enrollment and backup code regeneration * fix: require OTP verification for account deletion when 2FA is enabled * refactor: Improve code formatting and readability in TwoFactorController and UserController - Reformatted code in TwoFactorController and UserController for better readability by aligning parameters and breaking long lines. - Updated test cases in deleteUser.spec.js and TwoFactorController.spec.js to enhance clarity by formatting object parameters consistently. * refactor: Consolidate OTP and backup code verification logic in TwoFactorController and UserController - Introduced a new `verifyOTPOrBackupCode` function to streamline the verification process for TOTP tokens and backup codes across multiple controllers. - Updated the `enable2FA`, `disable2FA`, and `deleteUserController` methods to utilize the new verification function, enhancing code reusability and readability. - Adjusted related tests to reflect the changes in verification logic, ensuring consistent behavior across different scenarios. - Improved error handling and response messages for verification failures, providing clearer feedback to users. * chore: linting * refactor: Update BackupCodesItem component to enhance OTP verification logic - Consolidated OTP input handling by moving the 2FA verification UI logic to a more consistent location within the component. - Improved the state management for OTP readiness, ensuring the regenerate button is only enabled when the OTP is ready. - Cleaned up imports by removing redundant type imports, enhancing code clarity and maintainability. * chore: lint * fix: stage 2FA re-enrollment in pending fields to prevent disarmament window enable2FA now writes to pendingTotpSecret/pendingBackupCodes instead of overwriting the live fields. confirm2FA performs the atomic swap only after the new TOTP code is verified. If the user abandons mid-flow, their existing 2FA remains active and intact. --- api/server/controllers/TwoFactorController.js | 107 +++++-- api/server/controllers/UserController.js | 18 ++ .../__tests__/TwoFactorController.spec.js | 264 +++++++++++++++ .../controllers/__tests__/deleteUser.spec.js | 302 ++++++++++++++++++ api/server/routes/auth.js | 2 +- api/server/services/twoFactorService.js | 54 +++- .../SettingsTabs/Account/BackupCodesItem.tsx | 96 +++++- .../SettingsTabs/Account/DeleteAccount.tsx | 92 +++++- client/src/data-provider/Auth/mutations.ts | 23 +- client/src/locales/en/translation.json | 1 + packages/data-provider/src/data-service.ts | 14 +- packages/data-provider/src/types.ts | 43 ++- packages/data-schemas/src/schema/user.ts | 9 + packages/data-schemas/src/types/user.ts | 6 + 14 files changed, 927 insertions(+), 104 deletions(-) create mode 100644 api/server/controllers/__tests__/TwoFactorController.spec.js create mode 100644 api/server/controllers/__tests__/deleteUser.spec.js diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index fde5965261..18a0ee3f5a 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -1,5 +1,6 @@ const { encryptV3, logger } = require('@librechat/data-schemas'); const { + verifyOTPOrBackupCode, generateBackupCodes, generateTOTPSecret, verifyBackupCode, @@ -13,24 +14,42 @@ const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); /** * Enable 2FA for the user by generating a new TOTP secret and backup codes. * The secret is encrypted and stored, and 2FA is marked as disabled until confirmed. + * If 2FA is already enabled, requires OTP or backup code verification to re-enroll. */ const enable2FA = async (req, res) => { try { const userId = req.user.id; + const existingUser = await getUserById( + userId, + '+totpSecret +backupCodes _id twoFactorEnabled email', + ); + + if (existingUser && existingUser.twoFactorEnabled) { + const { token, backupCode } = req.body; + const result = await verifyOTPOrBackupCode({ + user: existingUser, + token, + backupCode, + persistBackupUse: false, + }); + + if (!result.verified) { + const msg = result.message ?? 'TOTP token or backup code is required to re-enroll 2FA'; + return res.status(result.status ?? 400).json({ message: msg }); + } + } + const secret = generateTOTPSecret(); const { plainCodes, codeObjects } = await generateBackupCodes(); - - // Encrypt the secret with v3 encryption before saving. const encryptedSecret = encryptV3(secret); - // Update the user record: store the secret & backup codes and set twoFactorEnabled to false. const user = await updateUser(userId, { - totpSecret: encryptedSecret, - backupCodes: codeObjects, - twoFactorEnabled: false, + pendingTotpSecret: encryptedSecret, + pendingBackupCodes: codeObjects, }); - const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; + const email = user.email || (existingUser && existingUser.email) || ''; + const otpauthUrl = `otpauth://totp/${safeAppTitle}:${email}?secret=${secret}&issuer=${safeAppTitle}`; return res.status(200).json({ otpauthUrl, backupCodes: plainCodes }); } catch (err) { @@ -46,13 +65,14 @@ const verify2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; - const user = await getUserById(userId, '_id totpSecret backupCodes'); + const user = await getUserById(userId, '+totpSecret +pendingTotpSecret +backupCodes _id'); + const secretSource = user?.pendingTotpSecret ?? user?.totpSecret; - if (!user || !user.totpSecret) { + if (!user || !secretSource) { return res.status(400).json({ message: '2FA not initiated' }); } - const secret = await getTOTPSecret(user.totpSecret); + const secret = await getTOTPSecret(secretSource); let isVerified = false; if (token) { @@ -78,15 +98,28 @@ const confirm2FA = async (req, res) => { try { const userId = req.user.id; const { token } = req.body; - const user = await getUserById(userId, '_id totpSecret'); + const user = await getUserById( + userId, + '+totpSecret +pendingTotpSecret +pendingBackupCodes _id', + ); + const secretSource = user?.pendingTotpSecret ?? user?.totpSecret; - if (!user || !user.totpSecret) { + if (!user || !secretSource) { return res.status(400).json({ message: '2FA not initiated' }); } - const secret = await getTOTPSecret(user.totpSecret); + const secret = await getTOTPSecret(secretSource); if (await verifyTOTP(secret, token)) { - await updateUser(userId, { twoFactorEnabled: true }); + const update = { + totpSecret: user.pendingTotpSecret ?? user.totpSecret, + twoFactorEnabled: true, + pendingTotpSecret: null, + pendingBackupCodes: [], + }; + if (user.pendingBackupCodes?.length) { + update.backupCodes = user.pendingBackupCodes; + } + await updateUser(userId, update); return res.status(200).json(); } return res.status(400).json({ message: 'Invalid token.' }); @@ -104,31 +137,27 @@ const disable2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; - const user = await getUserById(userId, '_id totpSecret backupCodes'); + const user = await getUserById(userId, '+totpSecret +backupCodes _id twoFactorEnabled'); if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA is not setup for this user' }); } if (user.twoFactorEnabled) { - const secret = await getTOTPSecret(user.totpSecret); - let isVerified = false; + const result = await verifyOTPOrBackupCode({ user, token, backupCode }); - if (token) { - isVerified = await verifyTOTP(secret, token); - } else if (backupCode) { - isVerified = await verifyBackupCode({ user, backupCode }); - } else { - return res - .status(400) - .json({ message: 'Either token or backup code is required to disable 2FA' }); - } - - if (!isVerified) { - return res.status(401).json({ message: 'Invalid token or backup code' }); + if (!result.verified) { + const msg = result.message ?? 'Either token or backup code is required to disable 2FA'; + return res.status(result.status ?? 400).json({ message: msg }); } } - await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false }); + await updateUser(userId, { + totpSecret: null, + backupCodes: [], + twoFactorEnabled: false, + pendingTotpSecret: null, + pendingBackupCodes: [], + }); return res.status(200).json(); } catch (err) { logger.error('[disable2FA]', err); @@ -138,10 +167,28 @@ const disable2FA = async (req, res) => { /** * Regenerate backup codes for the user. + * Requires OTP or backup code verification if 2FA is already enabled. */ const regenerateBackupCodes = async (req, res) => { try { const userId = req.user.id; + const user = await getUserById(userId, '+totpSecret +backupCodes _id twoFactorEnabled'); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + if (user.twoFactorEnabled) { + const { token, backupCode } = req.body; + const result = await verifyOTPOrBackupCode({ user, token, backupCode }); + + if (!result.verified) { + const msg = + result.message ?? 'TOTP token or backup code is required to regenerate backup codes'; + return res.status(result.status ?? 400).json({ message: msg }); + } + } + const { plainCodes, codeObjects } = await generateBackupCodes(); await updateUser(userId, { backupCodes: codeObjects }); return res.status(200).json({ diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 7a9dd8125e..b3160bb3d3 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -14,6 +14,7 @@ const { deleteMessages, deletePresets, deleteUserKey, + getUserById, deleteConvos, deleteFiles, updateUser, @@ -34,6 +35,7 @@ const { User, } = require('~/db/models'); const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); +const { verifyOTPOrBackupCode } = require('~/server/services/twoFactorService'); const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService'); const { getMCPManager, getFlowStateManager, getMCPServersRegistry } = require('~/config'); const { invalidateCachedTools } = require('~/server/services/Config/getCachedTools'); @@ -241,6 +243,22 @@ const deleteUserController = async (req, res) => { const { user } = req; try { + const existingUser = await getUserById( + user.id, + '+totpSecret +backupCodes _id twoFactorEnabled', + ); + if (existingUser && existingUser.twoFactorEnabled) { + const { token, backupCode } = req.body; + const result = await verifyOTPOrBackupCode({ user: existingUser, token, backupCode }); + + if (!result.verified) { + const msg = + result.message ?? + 'TOTP token or backup code is required to delete account with 2FA enabled'; + return res.status(result.status ?? 400).json({ message: msg }); + } + } + await deleteMessages({ user: user.id }); // delete user messages await deleteAllUserSessions({ userId: user.id }); // delete user sessions await Transaction.deleteMany({ user: user.id }); // delete user transactions diff --git a/api/server/controllers/__tests__/TwoFactorController.spec.js b/api/server/controllers/__tests__/TwoFactorController.spec.js new file mode 100644 index 0000000000..62531d94a1 --- /dev/null +++ b/api/server/controllers/__tests__/TwoFactorController.spec.js @@ -0,0 +1,264 @@ +const mockGetUserById = jest.fn(); +const mockUpdateUser = jest.fn(); +const mockVerifyOTPOrBackupCode = jest.fn(); +const mockGenerateTOTPSecret = jest.fn(); +const mockGenerateBackupCodes = jest.fn(); +const mockEncryptV3 = jest.fn(); + +jest.mock('@librechat/data-schemas', () => ({ + encryptV3: (...args) => mockEncryptV3(...args), + logger: { error: jest.fn() }, +})); + +jest.mock('~/server/services/twoFactorService', () => ({ + verifyOTPOrBackupCode: (...args) => mockVerifyOTPOrBackupCode(...args), + generateBackupCodes: (...args) => mockGenerateBackupCodes(...args), + generateTOTPSecret: (...args) => mockGenerateTOTPSecret(...args), + verifyBackupCode: jest.fn(), + getTOTPSecret: jest.fn(), + verifyTOTP: jest.fn(), +})); + +jest.mock('~/models', () => ({ + getUserById: (...args) => mockGetUserById(...args), + updateUser: (...args) => mockUpdateUser(...args), +})); + +const { enable2FA, regenerateBackupCodes } = require('~/server/controllers/TwoFactorController'); + +function createRes() { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +} + +const PLAIN_CODES = ['code1', 'code2', 'code3']; +const CODE_OBJECTS = [ + { codeHash: 'h1', used: false, usedAt: null }, + { codeHash: 'h2', used: false, usedAt: null }, + { codeHash: 'h3', used: false, usedAt: null }, +]; + +beforeEach(() => { + jest.clearAllMocks(); + mockGenerateTOTPSecret.mockReturnValue('NEWSECRET'); + mockGenerateBackupCodes.mockResolvedValue({ plainCodes: PLAIN_CODES, codeObjects: CODE_OBJECTS }); + mockEncryptV3.mockReturnValue('encrypted-secret'); +}); + +describe('enable2FA', () => { + it('allows first-time setup without token — writes to pending fields', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ _id: 'user1', twoFactorEnabled: false, email: 'a@b.com' }); + mockUpdateUser.mockResolvedValue({ email: 'a@b.com' }); + + await enable2FA(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ otpauthUrl: expect.any(String), backupCodes: PLAIN_CODES }), + ); + expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled(); + const updateCall = mockUpdateUser.mock.calls[0][1]; + expect(updateCall).toHaveProperty('pendingTotpSecret', 'encrypted-secret'); + expect(updateCall).toHaveProperty('pendingBackupCodes', CODE_OBJECTS); + expect(updateCall).not.toHaveProperty('twoFactorEnabled'); + expect(updateCall).not.toHaveProperty('totpSecret'); + expect(updateCall).not.toHaveProperty('backupCodes'); + }); + + it('re-enrollment writes to pending fields, leaving live 2FA intact', async () => { + const req = { user: { id: 'user1' }, body: { token: '123456' } }; + const res = createRes(); + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + email: 'a@b.com', + }; + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({ email: 'a@b.com' }); + + await enable2FA(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: '123456', + backupCode: undefined, + persistBackupUse: false, + }); + expect(res.status).toHaveBeenCalledWith(200); + const updateCall = mockUpdateUser.mock.calls[0][1]; + expect(updateCall).toHaveProperty('pendingTotpSecret', 'encrypted-secret'); + expect(updateCall).toHaveProperty('pendingBackupCodes', CODE_OBJECTS); + expect(updateCall).not.toHaveProperty('twoFactorEnabled'); + expect(updateCall).not.toHaveProperty('totpSecret'); + }); + + it('allows re-enrollment with valid backup code (persistBackupUse: false)', async () => { + const req = { user: { id: 'user1' }, body: { backupCode: 'backup123' } }; + const res = createRes(); + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + email: 'a@b.com', + }; + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({ email: 'a@b.com' }); + + await enable2FA(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith( + expect.objectContaining({ persistBackupUse: false }), + ); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('returns error when no token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 }); + + await enable2FA(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockUpdateUser).not.toHaveBeenCalled(); + }); + + it('returns 401 when invalid token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: { token: 'wrong' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await enable2FA(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' }); + expect(mockUpdateUser).not.toHaveBeenCalled(); + }); +}); + +describe('regenerateBackupCodes', () => { + it('returns 404 when user not found', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue(null); + + await regenerateBackupCodes(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'User not found' }); + }); + + it('requires OTP when 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: { token: '123456' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({}); + + await regenerateBackupCodes(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + backupCodes: PLAIN_CODES, + backupCodesHash: CODE_OBJECTS, + }); + }); + + it('returns error when no token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 }); + + await regenerateBackupCodes(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it('returns 401 when invalid token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: { token: 'wrong' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await regenerateBackupCodes(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' }); + }); + + it('includes backupCodesHash in response', async () => { + const req = { user: { id: 'user1' }, body: { token: '123456' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({}); + + await regenerateBackupCodes(req, res); + + const responseBody = res.json.mock.calls[0][0]; + expect(responseBody).toHaveProperty('backupCodesHash', CODE_OBJECTS); + expect(responseBody).toHaveProperty('backupCodes', PLAIN_CODES); + }); + + it('allows regeneration without token when 2FA is not enabled', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: false, + }); + mockUpdateUser.mockResolvedValue({}); + + await regenerateBackupCodes(req, res); + + expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + backupCodes: PLAIN_CODES, + backupCodesHash: CODE_OBJECTS, + }); + }); +}); diff --git a/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js new file mode 100644 index 0000000000..d0f54a046f --- /dev/null +++ b/api/server/controllers/__tests__/deleteUser.spec.js @@ -0,0 +1,302 @@ +const mockGetUserById = jest.fn(); +const mockDeleteMessages = jest.fn(); +const mockDeleteAllUserSessions = jest.fn(); +const mockDeleteUserById = jest.fn(); +const mockDeleteAllSharedLinks = jest.fn(); +const mockDeletePresets = jest.fn(); +const mockDeleteUserKey = jest.fn(); +const mockDeleteConvos = jest.fn(); +const mockDeleteFiles = jest.fn(); +const mockGetFiles = jest.fn(); +const mockUpdateUserPlugins = jest.fn(); +const mockUpdateUser = jest.fn(); +const mockFindToken = jest.fn(); +const mockVerifyOTPOrBackupCode = jest.fn(); +const mockDeleteUserPluginAuth = jest.fn(); +const mockProcessDeleteRequest = jest.fn(); +const mockDeleteToolCalls = jest.fn(); +const mockDeleteUserAgents = jest.fn(); +const mockDeleteUserPrompts = jest.fn(); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { error: jest.fn(), info: jest.fn() }, + webSearchKeys: [], +})); + +jest.mock('librechat-data-provider', () => ({ + Tools: {}, + CacheKeys: {}, + Constants: { mcp_delimiter: '::', mcp_prefix: 'mcp_' }, + FileSources: {}, +})); + +jest.mock('@librechat/api', () => ({ + MCPOAuthHandler: {}, + MCPTokenStorage: {}, + normalizeHttpError: jest.fn(), + extractWebSearchEnvVars: jest.fn(), +})); + +jest.mock('~/models', () => ({ + deleteAllUserSessions: (...args) => mockDeleteAllUserSessions(...args), + deleteAllSharedLinks: (...args) => mockDeleteAllSharedLinks(...args), + updateUserPlugins: (...args) => mockUpdateUserPlugins(...args), + deleteUserById: (...args) => mockDeleteUserById(...args), + deleteMessages: (...args) => mockDeleteMessages(...args), + deletePresets: (...args) => mockDeletePresets(...args), + deleteUserKey: (...args) => mockDeleteUserKey(...args), + getUserById: (...args) => mockGetUserById(...args), + deleteConvos: (...args) => mockDeleteConvos(...args), + deleteFiles: (...args) => mockDeleteFiles(...args), + updateUser: (...args) => mockUpdateUser(...args), + findToken: (...args) => mockFindToken(...args), + getFiles: (...args) => mockGetFiles(...args), +})); + +jest.mock('~/db/models', () => ({ + ConversationTag: { deleteMany: jest.fn() }, + AgentApiKey: { deleteMany: jest.fn() }, + Transaction: { deleteMany: jest.fn() }, + MemoryEntry: { deleteMany: jest.fn() }, + Assistant: { deleteMany: jest.fn() }, + AclEntry: { deleteMany: jest.fn() }, + Balance: { deleteMany: jest.fn() }, + Action: { deleteMany: jest.fn() }, + Group: { updateMany: jest.fn() }, + Token: { deleteMany: jest.fn() }, + User: {}, +})); + +jest.mock('~/server/services/PluginService', () => ({ + updateUserPluginAuth: jest.fn(), + deleteUserPluginAuth: (...args) => mockDeleteUserPluginAuth(...args), +})); + +jest.mock('~/server/services/twoFactorService', () => ({ + verifyOTPOrBackupCode: (...args) => mockVerifyOTPOrBackupCode(...args), +})); + +jest.mock('~/server/services/AuthService', () => ({ + verifyEmail: jest.fn(), + resendVerificationEmail: jest.fn(), +})); + +jest.mock('~/config', () => ({ + getMCPManager: jest.fn(), + getFlowStateManager: jest.fn(), + getMCPServersRegistry: jest.fn(), +})); + +jest.mock('~/server/services/Config/getCachedTools', () => ({ + invalidateCachedTools: jest.fn(), +})); + +jest.mock('~/server/services/Files/S3/crud', () => ({ + needsRefresh: jest.fn(), + getNewS3URL: jest.fn(), +})); + +jest.mock('~/server/services/Files/process', () => ({ + processDeleteRequest: (...args) => mockProcessDeleteRequest(...args), +})); + +jest.mock('~/server/services/Config', () => ({ + getAppConfig: jest.fn(), +})); + +jest.mock('~/models/ToolCall', () => ({ + deleteToolCalls: (...args) => mockDeleteToolCalls(...args), +})); + +jest.mock('~/models/Prompt', () => ({ + deleteUserPrompts: (...args) => mockDeleteUserPrompts(...args), +})); + +jest.mock('~/models/Agent', () => ({ + deleteUserAgents: (...args) => mockDeleteUserAgents(...args), +})); + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(), +})); + +const { deleteUserController } = require('~/server/controllers/UserController'); + +function createRes() { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.send = jest.fn().mockReturnValue(res); + return res; +} + +function stubDeletionMocks() { + mockDeleteMessages.mockResolvedValue(); + mockDeleteAllUserSessions.mockResolvedValue(); + mockDeleteUserKey.mockResolvedValue(); + mockDeletePresets.mockResolvedValue(); + mockDeleteConvos.mockResolvedValue(); + mockDeleteUserPluginAuth.mockResolvedValue(); + mockDeleteUserById.mockResolvedValue(); + mockDeleteAllSharedLinks.mockResolvedValue(); + mockGetFiles.mockResolvedValue([]); + mockProcessDeleteRequest.mockResolvedValue(); + mockDeleteFiles.mockResolvedValue(); + mockDeleteToolCalls.mockResolvedValue(); + mockDeleteUserAgents.mockResolvedValue(); + mockDeleteUserPrompts.mockResolvedValue(); +} + +beforeEach(() => { + jest.clearAllMocks(); + stubDeletionMocks(); +}); + +describe('deleteUserController - 2FA enforcement', () => { + it('proceeds with deletion when 2FA is not enabled', async () => { + const req = { user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ _id: 'user1', twoFactorEnabled: false }); + + await deleteUserController(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + expect(mockDeleteMessages).toHaveBeenCalled(); + expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled(); + }); + + it('proceeds with deletion when user has no 2FA record', async () => { + const req = { user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue(null); + + await deleteUserController(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + }); + + it('returns error when 2FA is enabled and verification fails with 400', async () => { + const req = { user: { id: 'user1', _id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 }); + + await deleteUserController(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockDeleteMessages).not.toHaveBeenCalled(); + }); + + it('returns 401 when 2FA is enabled and invalid TOTP token provided', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }; + const req = { user: { id: 'user1', _id: 'user1' }, body: { token: 'wrong' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: 'wrong', + backupCode: undefined, + }); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' }); + expect(mockDeleteMessages).not.toHaveBeenCalled(); + }); + + it('returns 401 when 2FA is enabled and invalid backup code provided', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + backupCodes: [], + }; + const req = { user: { id: 'user1', _id: 'user1' }, body: { backupCode: 'bad-code' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: undefined, + backupCode: 'bad-code', + }); + expect(res.status).toHaveBeenCalledWith(401); + expect(mockDeleteMessages).not.toHaveBeenCalled(); + }); + + it('deletes account when valid TOTP token provided with 2FA enabled', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }; + const req = { + user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, + body: { token: '123456' }, + }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: '123456', + backupCode: undefined, + }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + expect(mockDeleteMessages).toHaveBeenCalled(); + }); + + it('deletes account when valid backup code provided with 2FA enabled', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + backupCodes: [{ codeHash: 'h1', used: false }], + }; + const req = { + user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, + body: { backupCode: 'valid-code' }, + }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: undefined, + backupCode: 'valid-code', + }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + expect(mockDeleteMessages).toHaveBeenCalled(); + }); +}); diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index e84442f65f..d55684f3de 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -63,7 +63,7 @@ router.post( resetPasswordController, ); -router.get('/2fa/enable', middleware.requireJwtAuth, enable2FA); +router.post('/2fa/enable', middleware.requireJwtAuth, enable2FA); router.post('/2fa/verify', middleware.requireJwtAuth, verify2FA); router.post('/2fa/verify-temp', middleware.checkBan, verify2FAWithTempToken); router.post('/2fa/confirm', middleware.requireJwtAuth, confirm2FA); diff --git a/api/server/services/twoFactorService.js b/api/server/services/twoFactorService.js index cce24e2322..313c557133 100644 --- a/api/server/services/twoFactorService.js +++ b/api/server/services/twoFactorService.js @@ -153,9 +153,11 @@ const generateBackupCodes = async (count = 10) => { * @param {Object} params * @param {Object} params.user * @param {string} params.backupCode + * @param {boolean} [params.persist=true] - Whether to persist the used-mark to the database. + * Pass `false` when the caller will immediately overwrite `backupCodes` (e.g. re-enrollment). * @returns {Promise} */ -const verifyBackupCode = async ({ user, backupCode }) => { +const verifyBackupCode = async ({ user, backupCode, persist = true }) => { if (!backupCode || !user || !Array.isArray(user.backupCodes)) { return false; } @@ -165,17 +167,50 @@ const verifyBackupCode = async ({ user, backupCode }) => { (codeObj) => codeObj.codeHash === hashedInput && !codeObj.used, ); - if (matchingCode) { + if (!matchingCode) { + return false; + } + + if (persist) { const updatedBackupCodes = user.backupCodes.map((codeObj) => codeObj.codeHash === hashedInput && !codeObj.used ? { ...codeObj, used: true, usedAt: new Date() } : codeObj, ); - // Update the user record with the marked backup code. await updateUser(user._id, { backupCodes: updatedBackupCodes }); - return true; } - return false; + return true; +}; + +/** + * Verifies a user's identity via TOTP token or backup code. + * @param {Object} params + * @param {Object} params.user - The user document (must include totpSecret and backupCodes). + * @param {string} [params.token] - A 6-digit TOTP token. + * @param {string} [params.backupCode] - An 8-character backup code. + * @param {boolean} [params.persistBackupUse=true] - Whether to mark the backup code as used in the DB. + * @returns {Promise<{ verified: boolean, status?: number, message?: string }>} + */ +const verifyOTPOrBackupCode = async ({ user, token, backupCode, persistBackupUse = true }) => { + if (!token && !backupCode) { + return { verified: false, status: 400 }; + } + + if (token) { + const secret = await getTOTPSecret(user.totpSecret); + if (!secret) { + return { verified: false, status: 400, message: '2FA secret is missing or corrupted' }; + } + const ok = await verifyTOTP(secret, token); + return ok + ? { verified: true } + : { verified: false, status: 401, message: 'Invalid token or backup code' }; + } + + const ok = await verifyBackupCode({ user, backupCode, persist: persistBackupUse }); + return ok + ? { verified: true } + : { verified: false, status: 401, message: 'Invalid token or backup code' }; }; /** @@ -213,11 +248,12 @@ const generate2FATempToken = (userId) => { }; module.exports = { - generateTOTPSecret, - generateTOTP, - verifyTOTP, + verifyOTPOrBackupCode, + generate2FATempToken, generateBackupCodes, + generateTOTPSecret, verifyBackupCode, getTOTPSecret, - generate2FATempToken, + generateTOTP, + verifyTOTP, }; diff --git a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx index c89ce61fff..e66cb7b08a 100644 --- a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx @@ -1,12 +1,23 @@ import React, { useState } from 'react'; import { RefreshCcw } from 'lucide-react'; +import { useSetRecoilState } from 'recoil'; import { motion, AnimatePresence } from 'framer-motion'; -import { TBackupCode, TRegenerateBackupCodesResponse, type TUser } from 'librechat-data-provider'; +import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; +import type { + TRegenerateBackupCodesResponse, + TRegenerateBackupCodesRequest, + TBackupCode, + TUser, +} from 'librechat-data-provider'; import { - OGDialog, + InputOTPSeparator, + InputOTPGroup, + InputOTPSlot, OGDialogContent, OGDialogTitle, OGDialogTrigger, + OGDialog, + InputOTP, Button, Label, Spinner, @@ -15,7 +26,6 @@ import { } from '@librechat/client'; import { useRegenerateBackupCodesMutation } from '~/data-provider'; import { useAuthContext, useLocalize } from '~/hooks'; -import { useSetRecoilState } from 'recoil'; import store from '~/store'; const BackupCodesItem: React.FC = () => { @@ -24,25 +34,30 @@ const BackupCodesItem: React.FC = () => { const { showToast } = useToastContext(); const setUser = useSetRecoilState(store.user); const [isDialogOpen, setDialogOpen] = useState(false); + const [otpToken, setOtpToken] = useState(''); + const [useBackup, setUseBackup] = useState(false); const { mutate: regenerateBackupCodes, isLoading } = useRegenerateBackupCodesMutation(); + const needs2FA = !!user?.twoFactorEnabled; + const fetchBackupCodes = (auto: boolean = false) => { - regenerateBackupCodes(undefined, { + let payload: TRegenerateBackupCodesRequest | undefined; + if (needs2FA && otpToken.trim()) { + payload = useBackup ? { backupCode: otpToken.trim() } : { token: otpToken.trim() }; + } + + regenerateBackupCodes(payload, { onSuccess: (data: TRegenerateBackupCodesResponse) => { - const newBackupCodes: TBackupCode[] = data.backupCodesHash.map((codeHash) => ({ - codeHash, - used: false, - usedAt: null, - })); + const newBackupCodes: TBackupCode[] = data.backupCodesHash; setUser((prev) => ({ ...prev, backupCodes: newBackupCodes }) as TUser); + setOtpToken(''); showToast({ message: localize('com_ui_backup_codes_regenerated'), status: 'success', }); - // Trigger file download only when user explicitly clicks the button. if (!auto && newBackupCodes.length) { const codesString = data.backupCodes.join('\n'); const blob = new Blob([codesString], { type: 'text/plain;charset=utf-8' }); @@ -66,6 +81,8 @@ const BackupCodesItem: React.FC = () => { fetchBackupCodes(false); }; + const otpReady = !needs2FA || otpToken.length === (useBackup ? 8 : 6); + return (
@@ -161,10 +178,10 @@ const BackupCodesItem: React.FC = () => { ); })}
-
+
)} + {needs2FA && ( +
+ +
+ + {useBackup ? ( + + + + + + + + + + + ) : ( + <> + + + + + + + + + + + + + )} + +
+ +
+ )} diff --git a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx index e879a0f2c6..d9c432c6a2 100644 --- a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx @@ -1,16 +1,22 @@ -import { LockIcon, Trash } from 'lucide-react'; import React, { useState, useCallback } from 'react'; +import { LockIcon, Trash } from 'lucide-react'; +import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; import { - Label, - Input, - Button, - Spinner, - OGDialog, + InputOTPSeparator, OGDialogContent, OGDialogTrigger, OGDialogHeader, + InputOTPGroup, OGDialogTitle, + InputOTPSlot, + OGDialog, + InputOTP, + Spinner, + Button, + Label, + Input, } from '@librechat/client'; +import type { TDeleteUserRequest } from 'librechat-data-provider'; import { useDeleteUserMutation } from '~/data-provider'; import { useAuthContext } from '~/hooks/AuthContext'; import { LocalizeFunction } from '~/common'; @@ -21,16 +27,27 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea const localize = useLocalize(); const { user, logout } = useAuthContext(); const { mutate: deleteUser, isLoading: isDeleting } = useDeleteUserMutation({ - onMutate: () => logout(), + onSuccess: () => logout(), }); const [isDialogOpen, setDialogOpen] = useState(false); const [isLocked, setIsLocked] = useState(true); + const [otpToken, setOtpToken] = useState(''); + const [useBackup, setUseBackup] = useState(false); + + const needs2FA = !!user?.twoFactorEnabled; const handleDeleteUser = () => { - if (!isLocked) { - deleteUser(undefined); + if (isLocked) { + return; } + + let payload: TDeleteUserRequest | undefined; + if (needs2FA && otpToken.trim()) { + payload = useBackup ? { backupCode: otpToken.trim() } : { token: otpToken.trim() }; + } + + deleteUser(payload); }; const handleInputChange = useCallback( @@ -42,6 +59,8 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea [user?.email], ); + const otpReady = !needs2FA || otpToken.length === (useBackup ? 8 : 6); + return ( <> @@ -79,7 +98,60 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea (e) => handleInputChange(e.target.value), )}
- {renderDeleteButton(handleDeleteUser, isDeleting, isLocked, localize)} + {needs2FA && ( +
+ +
+ + {useBackup ? ( + + + + + + + + + + + ) : ( + <> + + + + + + + + + + + + + )} + +
+ +
+ )} + {renderDeleteButton(handleDeleteUser, isDeleting, isLocked || !otpReady, localize)}
diff --git a/client/src/data-provider/Auth/mutations.ts b/client/src/data-provider/Auth/mutations.ts index 298ddd9b64..9930e42b4f 100644 --- a/client/src/data-provider/Auth/mutations.ts +++ b/client/src/data-provider/Auth/mutations.ts @@ -68,14 +68,14 @@ export const useRefreshTokenMutation = ( /* User */ export const useDeleteUserMutation = ( - options?: t.MutationOptions, -): UseMutationResult => { + options?: t.MutationOptions, +): UseMutationResult => { const queryClient = useQueryClient(); const clearStates = useClearStates(); const resetDefaultPreset = useResetRecoilState(store.defaultPreset); return useMutation([MutationKeys.deleteUser], { - mutationFn: () => dataService.deleteUser(), + mutationFn: (payload?: t.TDeleteUserRequest) => dataService.deleteUser(payload), ...(options || {}), onSuccess: (...args) => { resetDefaultPreset(); @@ -90,11 +90,11 @@ export const useDeleteUserMutation = ( export const useEnableTwoFactorMutation = (): UseMutationResult< t.TEnable2FAResponse, unknown, - void, + t.TEnable2FARequest | undefined, unknown > => { const queryClient = useQueryClient(); - return useMutation(() => dataService.enableTwoFactor(), { + return useMutation((payload?: t.TEnable2FARequest) => dataService.enableTwoFactor(payload), { onSuccess: (data) => { queryClient.setQueryData([QueryKeys.user, '2fa'], data); }, @@ -146,15 +146,18 @@ export const useDisableTwoFactorMutation = (): UseMutationResult< export const useRegenerateBackupCodesMutation = (): UseMutationResult< t.TRegenerateBackupCodesResponse, unknown, - void, + t.TRegenerateBackupCodesRequest | undefined, unknown > => { const queryClient = useQueryClient(); - return useMutation(() => dataService.regenerateBackupCodes(), { - onSuccess: (data) => { - queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data); + return useMutation( + (payload?: t.TRegenerateBackupCodesRequest) => dataService.regenerateBackupCodes(payload), + { + onSuccess: (data) => { + queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data); + }, }, - }); + ); }; export const useVerifyTwoFactorTempMutation = ( diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 35d8300489..196ea2ad4a 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -639,6 +639,7 @@ "com_ui_2fa_generate_error": "There was an error generating two-factor authentication settings", "com_ui_2fa_invalid": "Invalid two-factor authentication code", "com_ui_2fa_setup": "Setup 2FA", + "com_ui_2fa_verification_required": "Enter your 2FA code to continue", "com_ui_2fa_verified": "Successfully verified Two-Factor Authentication", "com_ui_accept": "I accept", "com_ui_action_button": "Action Button", diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index be5cccd43b..2c7a402d1f 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -21,8 +21,8 @@ export function revokeAllUserKeys(): Promise { return request.delete(endpoints.revokeAllUserKeys()); } -export function deleteUser(): Promise { - return request.delete(endpoints.deleteUser()); +export function deleteUser(payload?: t.TDeleteUserRequest): Promise { + return request.deleteWithOptions(endpoints.deleteUser(), { data: payload }); } export type FavoriteItem = { @@ -970,8 +970,8 @@ export function updateFeedback( } // 2FA -export function enableTwoFactor(): Promise { - return request.get(endpoints.enableTwoFactor()); +export function enableTwoFactor(payload?: t.TEnable2FARequest): Promise { + return request.post(endpoints.enableTwoFactor(), payload); } export function verifyTwoFactor(payload: t.TVerify2FARequest): Promise { @@ -986,8 +986,10 @@ export function disableTwoFactor(payload?: t.TDisable2FARequest): Promise { - return request.post(endpoints.regenerateBackupCodes()); +export function regenerateBackupCodes( + payload?: t.TRegenerateBackupCodesRequest, +): Promise { + return request.post(endpoints.regenerateBackupCodes(), payload); } export function verifyTwoFactorTemp( diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 3b04c40f45..5895fba321 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -425,28 +425,29 @@ export type TLoginResponse = { tempToken?: string; }; +/** Shared payload for any operation that requires OTP or backup-code verification. */ +export type TOTPVerificationPayload = { + token?: string; + backupCode?: string; +}; + +export type TEnable2FARequest = TOTPVerificationPayload; + export type TEnable2FAResponse = { otpauthUrl: string; backupCodes: string[]; message?: string; }; -export type TVerify2FARequest = { - token?: string; - backupCode?: string; -}; +export type TVerify2FARequest = TOTPVerificationPayload; export type TVerify2FAResponse = { message: string; }; -/** - * For verifying 2FA during login with a temporary token. - */ -export type TVerify2FATempRequest = { +/** For verifying 2FA during login with a temporary token. */ +export type TVerify2FATempRequest = TOTPVerificationPayload & { tempToken: string; - token?: string; - backupCode?: string; }; export type TVerify2FATempResponse = { @@ -455,30 +456,22 @@ export type TVerify2FATempResponse = { message?: string; }; -/** - * Request for disabling 2FA. - */ -export type TDisable2FARequest = { - token?: string; - backupCode?: string; -}; +export type TDisable2FARequest = TOTPVerificationPayload; -/** - * Response from disabling 2FA. - */ export type TDisable2FAResponse = { message: string; }; -/** - * Response from regenerating backup codes. - */ +export type TRegenerateBackupCodesRequest = TOTPVerificationPayload; + export type TRegenerateBackupCodesResponse = { - message: string; + message?: string; backupCodes: string[]; - backupCodesHash: string[]; + backupCodesHash: TBackupCode[]; }; +export type TDeleteUserRequest = TOTPVerificationPayload; + export type TRequestPasswordReset = { email: string; }; diff --git a/packages/data-schemas/src/schema/user.ts b/packages/data-schemas/src/schema/user.ts index c2bdc6fd34..57c8f8574e 100644 --- a/packages/data-schemas/src/schema/user.ts +++ b/packages/data-schemas/src/schema/user.ts @@ -121,6 +121,15 @@ const userSchema = new Schema( type: [BackupCodeSchema], select: false, }, + pendingTotpSecret: { + type: String, + select: false, + }, + pendingBackupCodes: { + type: [BackupCodeSchema], + select: false, + default: undefined, + }, refreshToken: { type: [SessionSchema], }, diff --git a/packages/data-schemas/src/types/user.ts b/packages/data-schemas/src/types/user.ts index a78c4679f2..e1cecb7518 100644 --- a/packages/data-schemas/src/types/user.ts +++ b/packages/data-schemas/src/types/user.ts @@ -26,6 +26,12 @@ export interface IUser extends Document { used: boolean; usedAt?: Date | null; }>; + pendingTotpSecret?: string; + pendingBackupCodes?: Array<{ + codeHash: string; + used: boolean; + usedAt?: Date | null; + }>; refreshToken?: Array<{ refreshToken: string; }>; From c6982dc180c26d6c26e1802ee2c63b9dcf3046ee Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 02:57:56 -0400 Subject: [PATCH 028/111] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20Agent=20?= =?UTF-8?q?Permission=20Check=20on=20Image=20Upload=20Route=20(#12219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add agent permission check to image upload route * refactor: remove unused SystemRoles import and format test file for clarity * fix: address review findings for image upload agent permission check * refactor: move agent upload auth logic to TypeScript in packages/api Extract pure authorization logic from agentPermCheck.js into checkAgentUploadAuth() in packages/api/src/files/agentUploadAuth.ts. The function returns a structured result ({ allowed, status, error }) instead of writing HTTP responses directly, eliminating the dual responsibility and confusing sentinel return value. The JS wrapper in /api is now a thin adapter that translates the result to HTTP. * test: rewrite image upload permission tests as integration tests Replace mock-heavy images-agent-perm.spec.js with integration tests using MongoMemoryServer, real models, and real PermissionService. Follows the established pattern in files.agents.test.js. Moves test to sibling location (images.agents.test.js) matching backend convention. Adds temp file cleanup assertions on 403/404 responses and covers message_file exemption paths (boolean true, string "true", false). * fix: widen AgentUploadAuthDeps types to accept ObjectId from Mongoose The injected getAgent returns Mongoose documents where _id and author are Types.ObjectId at runtime, not string. Widen the DI interface to accept string | Types.ObjectId for _id, author, and resourceId so the contract accurately reflects real callers. * chore: move agent upload auth into files/agents/ subdirectory * refactor: delete agentPermCheck.js wrapper, move verifyAgentUploadPermission to packages/api The /api-only dependencies (getAgent, checkPermission) are now passed as object-field params from the route call sites. Both images.js and files.js import verifyAgentUploadPermission from @librechat/api and inject the deps directly, eliminating the intermediate JS wrapper. * style: fix import type ordering in agent upload auth * fix: prevent token TTL race in MCPTokenStorage.storeTokens When expires_in is provided, use it directly instead of round-tripping through Date arithmetic. The previous code computed accessTokenExpiry as a Date, then after an async encryptV2 call, recomputed expiresIn by subtracting Date.now(). On loaded CI runners the elapsed time caused Math.floor to truncate to 0, triggering the 1-year fallback and making the token appear permanently valid — so refresh never fired. --- api/server/routes/files/files.js | 53 +-- api/server/routes/files/images.agents.test.js | 376 ++++++++++++++++++ api/server/routes/files/images.js | 13 + packages/api/src/files/agents/auth.ts | 113 ++++++ packages/api/src/files/agents/index.ts | 1 + packages/api/src/files/index.ts | 1 + packages/api/src/mcp/oauth/tokens.ts | 28 +- 7 files changed, 525 insertions(+), 60 deletions(-) create mode 100644 api/server/routes/files/images.agents.test.js create mode 100644 packages/api/src/files/agents/auth.ts create mode 100644 packages/api/src/files/agents/index.ts diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index 5de2ddb379..9290d1a7ed 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -2,12 +2,12 @@ const fs = require('fs').promises; const express = require('express'); const { EnvVar } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); +const { verifyAgentUploadPermission } = require('@librechat/api'); const { Time, isUUID, CacheKeys, FileSources, - SystemRoles, ResourceType, EModelEndpoint, PermissionBits, @@ -381,48 +381,15 @@ router.post('/', async (req, res) => { return await processFileUpload({ req, res, metadata }); } - /** - * Check agent permissions for permanent agent file uploads (not message attachments). - * Message attachments (message_file=true) are temporary files for a single conversation - * and should be allowed for users who can chat with the agent. - * Permanent file uploads to tool_resources require EDIT permission. - */ - const isMessageAttachment = metadata.message_file === true || metadata.message_file === 'true'; - if (metadata.agent_id && metadata.tool_resource && !isMessageAttachment) { - const userId = req.user.id; - - /** Admin users bypass permission checks */ - if (req.user.role !== SystemRoles.ADMIN) { - const agent = await getAgent({ id: metadata.agent_id }); - - if (!agent) { - return res.status(404).json({ - error: 'Not Found', - message: 'Agent not found', - }); - } - - /** Check if user is the author or has edit permission */ - if (agent.author.toString() !== userId) { - const hasEditPermission = await checkPermission({ - userId, - role: req.user.role, - resourceType: ResourceType.AGENT, - resourceId: agent._id, - requiredPermission: PermissionBits.EDIT, - }); - - if (!hasEditPermission) { - logger.warn( - `[/files] User ${userId} denied upload to agent ${metadata.agent_id} (insufficient permissions)`, - ); - return res.status(403).json({ - error: 'Forbidden', - message: 'Insufficient permissions to upload files to this agent', - }); - } - } - } + const denied = await verifyAgentUploadPermission({ + req, + res, + metadata, + getAgent, + checkPermission, + }); + if (denied) { + return; } return await processAgentFileUpload({ req, res, metadata }); diff --git a/api/server/routes/files/images.agents.test.js b/api/server/routes/files/images.agents.test.js new file mode 100644 index 0000000000..862ab87d63 --- /dev/null +++ b/api/server/routes/files/images.agents.test.js @@ -0,0 +1,376 @@ +const express = require('express'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); +const { createMethods } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { + SystemRoles, + AccessRoleIds, + ResourceType, + PrincipalType, +} = require('librechat-data-provider'); +const { createAgent } = require('~/models/Agent'); + +jest.mock('~/server/services/Files/process', () => ({ + processAgentFileUpload: jest.fn().mockImplementation(async ({ res }) => { + return res.status(200).json({ message: 'Agent file uploaded', file_id: 'test-file-id' }); + }), + processImageFile: jest.fn().mockImplementation(async ({ res }) => { + return res.status(200).json({ message: 'Image processed' }); + }), + filterFile: jest.fn(), +})); + +jest.mock('fs', () => { + const actualFs = jest.requireActual('fs'); + return { + ...actualFs, + promises: { + ...actualFs.promises, + unlink: jest.fn().mockResolvedValue(undefined), + }, + }; +}); + +const fs = require('fs'); +const { processAgentFileUpload } = require('~/server/services/Files/process'); + +const router = require('~/server/routes/files/images'); + +describe('POST /images - Agent Upload Permission Check (Integration)', () => { + let mongoServer; + let authorId; + let otherUserId; + let agentCustomId; + let User; + let Agent; + let AclEntry; + let methods; + let modelsToCleanup = []; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + + const { createModels } = require('@librechat/data-schemas'); + const models = createModels(mongoose); + modelsToCleanup = Object.keys(models); + Object.assign(mongoose.models, models); + methods = createMethods(mongoose); + + User = models.User; + Agent = models.Agent; + AclEntry = models.AclEntry; + + await methods.seedDefaultRoles(); + }); + + afterAll(async () => { + const collections = mongoose.connection.collections; + for (const key in collections) { + await collections[key].deleteMany({}); + } + for (const modelName of modelsToCleanup) { + if (mongoose.models[modelName]) { + delete mongoose.models[modelName]; + } + } + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await Agent.deleteMany({}); + await User.deleteMany({}); + await AclEntry.deleteMany({}); + + authorId = new mongoose.Types.ObjectId(); + otherUserId = new mongoose.Types.ObjectId(); + agentCustomId = `agent_${uuidv4().replace(/-/g, '').substring(0, 21)}`; + + await User.create({ _id: authorId, username: 'author', email: 'author@test.com' }); + await User.create({ _id: otherUserId, username: 'other', email: 'other@test.com' }); + + jest.clearAllMocks(); + }); + + const createAppWithUser = (userId, userRole = SystemRoles.USER) => { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + if (req.method === 'POST') { + req.file = { + originalname: 'test.png', + mimetype: 'image/png', + size: 100, + path: '/tmp/t.png', + filename: 'test.png', + }; + req.file_id = uuidv4(); + } + next(); + }); + app.use((req, _res, next) => { + req.user = { id: userId.toString(), role: userRole }; + req.app = { locals: {} }; + req.config = { fileStrategy: 'local', paths: { imageOutput: '/tmp/images' } }; + next(); + }); + app.use('/images', router); + return app; + }; + + it('should return 403 when user has no permission on agent', async () => { + await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); + + it('should allow upload for agent owner', async () => { + await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const app = createAppWithUser(authorId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should allow upload for admin regardless of ownership', async () => { + await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const app = createAppWithUser(otherUserId, SystemRoles.ADMIN); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should allow upload for user with EDIT permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_EDITOR, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should deny upload for user with only VIEW permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); + + it('should skip permission check for regular image uploads without agent_id/tool_resource', async () => { + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + }); + + it('should return 404 for non-existent agent', async () => { + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: 'agent_nonexistent123456789', + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Not Found'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); + + it('should allow message_file attachment (boolean true) without EDIT permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + message_file: true, + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should allow message_file attachment (string "true") without EDIT permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + message_file: 'true', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should deny upload when message_file is false (not a message attachment)', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + message_file: false, + file_id: uuidv4(), + }); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); +}); diff --git a/api/server/routes/files/images.js b/api/server/routes/files/images.js index 8072612a69..185ec7a671 100644 --- a/api/server/routes/files/images.js +++ b/api/server/routes/files/images.js @@ -2,12 +2,15 @@ const path = require('path'); const fs = require('fs').promises; const express = require('express'); const { logger } = require('@librechat/data-schemas'); +const { verifyAgentUploadPermission } = require('@librechat/api'); const { isAssistantsEndpoint } = require('librechat-data-provider'); const { processAgentFileUpload, processImageFile, filterFile, } = require('~/server/services/Files/process'); +const { checkPermission } = require('~/server/services/PermissionService'); +const { getAgent } = require('~/models/Agent'); const router = express.Router(); @@ -22,6 +25,16 @@ router.post('/', async (req, res) => { metadata.file_id = req.file_id; if (!isAssistantsEndpoint(metadata.endpoint) && metadata.tool_resource != null) { + const denied = await verifyAgentUploadPermission({ + req, + res, + metadata, + getAgent, + checkPermission, + }); + if (denied) { + return; + } return await processAgentFileUpload({ req, res, metadata }); } diff --git a/packages/api/src/files/agents/auth.ts b/packages/api/src/files/agents/auth.ts new file mode 100644 index 0000000000..d9fb2b7423 --- /dev/null +++ b/packages/api/src/files/agents/auth.ts @@ -0,0 +1,113 @@ +import type { IUser } from '@librechat/data-schemas'; +import type { Response } from 'express'; +import type { Types } from 'mongoose'; +import { logger } from '@librechat/data-schemas'; +import { SystemRoles, ResourceType, PermissionBits } from 'librechat-data-provider'; +import type { ServerRequest } from '~/types'; + +export type AgentUploadAuthResult = + | { allowed: true } + | { allowed: false; status: number; error: string; message: string }; + +export interface AgentUploadAuthParams { + userId: string; + userRole: string; + agentId?: string; + toolResource?: string | null; + messageFile?: boolean | string; +} + +export interface AgentUploadAuthDeps { + getAgent: (params: { id: string }) => Promise<{ + _id: string | Types.ObjectId; + author?: string | Types.ObjectId | null; + } | null>; + checkPermission: (params: { + userId: string; + role: string; + resourceType: ResourceType; + resourceId: string | Types.ObjectId; + requiredPermission: number; + }) => Promise; +} + +export async function checkAgentUploadAuth( + params: AgentUploadAuthParams, + deps: AgentUploadAuthDeps, +): Promise { + const { userId, userRole, agentId, toolResource, messageFile } = params; + const { getAgent, checkPermission } = deps; + + const isMessageAttachment = messageFile === true || messageFile === 'true'; + if (!agentId || toolResource == null || isMessageAttachment) { + return { allowed: true }; + } + + if (userRole === SystemRoles.ADMIN) { + return { allowed: true }; + } + + const agent = await getAgent({ id: agentId }); + if (!agent) { + return { allowed: false, status: 404, error: 'Not Found', message: 'Agent not found' }; + } + + if (agent.author?.toString() === userId) { + return { allowed: true }; + } + + const hasEditPermission = await checkPermission({ + userId, + role: userRole, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + requiredPermission: PermissionBits.EDIT, + }); + + if (hasEditPermission) { + return { allowed: true }; + } + + logger.warn( + `[agentUploadAuth] User ${userId} denied upload to agent ${agentId} (insufficient permissions)`, + ); + return { + allowed: false, + status: 403, + error: 'Forbidden', + message: 'Insufficient permissions to upload files to this agent', + }; +} + +/** @returns true if denied (response already sent), false if allowed */ +export async function verifyAgentUploadPermission({ + req, + res, + metadata, + getAgent, + checkPermission, +}: { + req: ServerRequest; + res: Response; + metadata: { agent_id?: string; tool_resource?: string | null; message_file?: boolean | string }; + getAgent: AgentUploadAuthDeps['getAgent']; + checkPermission: AgentUploadAuthDeps['checkPermission']; +}): Promise { + const user = req.user as IUser; + const result = await checkAgentUploadAuth( + { + userId: user.id, + userRole: user.role ?? '', + agentId: metadata.agent_id, + toolResource: metadata.tool_resource, + messageFile: metadata.message_file, + }, + { getAgent, checkPermission }, + ); + + if (!result.allowed) { + res.status(result.status).json({ error: result.error, message: result.message }); + return true; + } + return false; +} diff --git a/packages/api/src/files/agents/index.ts b/packages/api/src/files/agents/index.ts new file mode 100644 index 0000000000..269586ee8b --- /dev/null +++ b/packages/api/src/files/agents/index.ts @@ -0,0 +1 @@ +export * from './auth'; diff --git a/packages/api/src/files/index.ts b/packages/api/src/files/index.ts index 707f2ef7fb..c3bdb49478 100644 --- a/packages/api/src/files/index.ts +++ b/packages/api/src/files/index.ts @@ -1,3 +1,4 @@ +export * from './agents'; export * from './audio'; export * from './context'; export * from './documents/crud'; diff --git a/packages/api/src/mcp/oauth/tokens.ts b/packages/api/src/mcp/oauth/tokens.ts index 7b1d189347..6094a05386 100644 --- a/packages/api/src/mcp/oauth/tokens.ts +++ b/packages/api/src/mcp/oauth/tokens.ts @@ -83,46 +83,40 @@ export class MCPTokenStorage { `${logPrefix} Token expires_in: ${'expires_in' in tokens ? tokens.expires_in : 'N/A'}, expires_at: ${'expires_at' in tokens ? tokens.expires_at : 'N/A'}`, ); - // Handle both expires_in and expires_at formats + const defaultTTL = 365 * 24 * 60 * 60; + let accessTokenExpiry: Date; + let expiresInSeconds: number; if ('expires_at' in tokens && tokens.expires_at) { /** MCPOAuthTokens format - already has calculated expiry */ logger.debug(`${logPrefix} Using expires_at: ${tokens.expires_at}`); accessTokenExpiry = new Date(tokens.expires_at); + expiresInSeconds = Math.floor((accessTokenExpiry.getTime() - Date.now()) / 1000); } else if (tokens.expires_in) { - /** Standard OAuthTokens format - calculate expiry */ + /** Standard OAuthTokens format - use expires_in directly to avoid lossy Date round-trip */ logger.debug(`${logPrefix} Using expires_in: ${tokens.expires_in}`); + expiresInSeconds = tokens.expires_in; accessTokenExpiry = new Date(Date.now() + tokens.expires_in * 1000); } else { - /** No expiry provided - default to 1 year */ logger.debug(`${logPrefix} No expiry provided, using default`); - accessTokenExpiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); + expiresInSeconds = defaultTTL; + accessTokenExpiry = new Date(Date.now() + defaultTTL * 1000); } logger.debug(`${logPrefix} Calculated expiry date: ${accessTokenExpiry.toISOString()}`); - logger.debug( - `${logPrefix} Date object: ${JSON.stringify({ - time: accessTokenExpiry.getTime(), - valid: !isNaN(accessTokenExpiry.getTime()), - iso: accessTokenExpiry.toISOString(), - })}`, - ); - // Ensure the date is valid before passing to createToken if (isNaN(accessTokenExpiry.getTime())) { logger.error(`${logPrefix} Invalid expiry date calculated, using default`); - accessTokenExpiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); + accessTokenExpiry = new Date(Date.now() + defaultTTL * 1000); + expiresInSeconds = defaultTTL; } - // Calculate expiresIn (seconds from now) - const expiresIn = Math.floor((accessTokenExpiry.getTime() - Date.now()) / 1000); - const accessTokenData = { userId, type: 'mcp_oauth', identifier, token: encryptedAccessToken, - expiresIn: expiresIn > 0 ? expiresIn : 365 * 24 * 60 * 60, // Default to 1 year if negative + expiresIn: expiresInSeconds > 0 ? expiresInSeconds : defaultTTL, }; // Check if token already exists and update if it does From 35a35dc2e9280e0bad29c5bb586bb0fd72d4104d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 03:06:29 -0400 Subject: [PATCH 029/111] =?UTF-8?q?=F0=9F=93=8F=20refactor:=20Add=20File?= =?UTF-8?q?=20Size=20Limits=20to=20Conversation=20Imports=20(#12221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add file size limits to conversation import multer instance * fix: address review findings for conversation import file size limits * fix: use local jest.mock for data-schemas instead of global moduleNameMapper The global @librechat/data-schemas mock in jest.config.js only provided logger, breaking all tests that depend on createModels from the same package. Replace with a virtual jest.mock scoped to the import spec file. * fix: move import to top of file, pre-compute upload middleware, assert logger.warn in tests * refactor: move resolveImportMaxFileSize to packages/api New backend logic belongs in packages/api as TypeScript. Delete the api/server/utils/import/limits.js wrapper and import directly from @librechat/api in convos.js and importConversations.js. Resolver unit tests move to packages/api; the api/ spec retains only multer behavior tests. * chore: rename importLimits to import * fix: stale type reference and mock isolation in import tests Update typeof import path from '../importLimits' to '../import' after the rename. Clear mockLogger.warn in beforeEach to prevent cross-test accumulation. * fix: add resolveImportMaxFileSize to @librechat/api mock in convos.spec.js * fix: resolve jest.mock hoisting issue in import tests jest.mock factories are hoisted above const declarations, so the mockLogger reference was undefined at factory evaluation time. Use a direct import of the mocked logger module instead. * fix: remove virtual flag from data-schemas mock for CI compatibility virtual: true prevents the mock from intercepting the real module in CI where @librechat/data-schemas is built, causing import.ts to use the real logger while the test asserts against the mock. --- api/jest.config.js | 2 +- .../__test-utils__/convos-route-mocks.js | 1 + .../routes/__tests__/convos-import.spec.js | 98 +++++++++++++++++++ api/server/routes/convos.js | 24 ++++- .../utils/import/importConversations.js | 8 +- .../api/src/utils/__tests__/import.test.ts | 76 ++++++++++++++ packages/api/src/utils/import.ts | 20 ++++ packages/api/src/utils/index.ts | 1 + 8 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 api/server/routes/__tests__/convos-import.spec.js create mode 100644 packages/api/src/utils/__tests__/import.test.ts create mode 100644 packages/api/src/utils/import.ts diff --git a/api/jest.config.js b/api/jest.config.js index 3b752403c1..47f8b7287b 100644 --- a/api/jest.config.js +++ b/api/jest.config.js @@ -9,7 +9,7 @@ module.exports = { moduleNameMapper: { '~/(.*)': '/$1', '~/data/auth.json': '/__mocks__/auth.mock.json', - '^openid-client/passport$': '/test/__mocks__/openid-client-passport.js', // Mock for the passport strategy part + '^openid-client/passport$': '/test/__mocks__/openid-client-passport.js', '^openid-client$': '/test/__mocks__/openid-client.js', }, transformIgnorePatterns: ['/node_modules/(?!(openid-client|oauth4webapi|jose)/).*/'], diff --git a/api/server/routes/__test-utils__/convos-route-mocks.js b/api/server/routes/__test-utils__/convos-route-mocks.js index ca5bafeda9..f89b77db3f 100644 --- a/api/server/routes/__test-utils__/convos-route-mocks.js +++ b/api/server/routes/__test-utils__/convos-route-mocks.js @@ -3,6 +3,7 @@ module.exports = { api: (overrides = {}) => ({ isEnabled: jest.fn(), + resolveImportMaxFileSize: jest.fn(() => 262144000), createAxiosInstance: jest.fn(() => ({ get: jest.fn(), post: jest.fn(), diff --git a/api/server/routes/__tests__/convos-import.spec.js b/api/server/routes/__tests__/convos-import.spec.js new file mode 100644 index 0000000000..c4ea139931 --- /dev/null +++ b/api/server/routes/__tests__/convos-import.spec.js @@ -0,0 +1,98 @@ +const express = require('express'); +const request = require('supertest'); +const multer = require('multer'); + +const importFileFilter = (req, file, cb) => { + if (file.mimetype === 'application/json') { + cb(null, true); + } else { + cb(new Error('Only JSON files are allowed'), false); + } +}; + +/** Proxy app that mirrors the production multer + error-handling pattern */ +function createImportApp(fileSize) { + const app = express(); + const upload = multer({ + storage: multer.memoryStorage(), + fileFilter: importFileFilter, + limits: { fileSize }, + }); + const uploadSingle = upload.single('file'); + + function handleUpload(req, res, next) { + uploadSingle(req, res, (err) => { + if (err && err.code === 'LIMIT_FILE_SIZE') { + return res.status(413).json({ message: 'File exceeds the maximum allowed size' }); + } + if (err) { + return next(err); + } + next(); + }); + } + + app.post('/import', handleUpload, (req, res) => { + res.status(201).json({ message: 'success', size: req.file.size }); + }); + + app.use((err, _req, res, _next) => { + res.status(400).json({ error: err.message }); + }); + + return app; +} + +describe('Conversation Import - Multer File Size Limits', () => { + describe('multer rejects files exceeding the configured limit', () => { + it('returns 413 for files larger than the limit', async () => { + const limit = 1024; + const app = createImportApp(limit); + const oversized = Buffer.alloc(limit + 512, 'x'); + + const res = await request(app) + .post('/import') + .attach('file', oversized, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(413); + expect(res.body.message).toBe('File exceeds the maximum allowed size'); + }); + + it('accepts files within the limit', async () => { + const limit = 4096; + const app = createImportApp(limit); + const valid = Buffer.from(JSON.stringify({ title: 'test' })); + + const res = await request(app) + .post('/import') + .attach('file', valid, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(201); + expect(res.body.message).toBe('success'); + }); + + it('rejects at the exact boundary (limit + 1 byte)', async () => { + const limit = 512; + const app = createImportApp(limit); + const boundary = Buffer.alloc(limit + 1, 'a'); + + const res = await request(app) + .post('/import') + .attach('file', boundary, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(413); + }); + + it('accepts a file just under the limit', async () => { + const limit = 512; + const app = createImportApp(limit); + const underLimit = Buffer.alloc(limit - 1, 'b'); + + const res = await request(app) + .post('/import') + .attach('file', underLimit, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(201); + }); + }); +}); diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 5f0c35fa0a..578796170a 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -1,7 +1,7 @@ const multer = require('multer'); const express = require('express'); const { sleep } = require('@librechat/agents'); -const { isEnabled } = require('@librechat/api'); +const { isEnabled, resolveImportMaxFileSize } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys, EModelEndpoint } = require('librechat-data-provider'); const { @@ -226,7 +226,25 @@ router.post('/update', validateConvoAccess, async (req, res) => { const { importIpLimiter, importUserLimiter } = createImportLimiters(); /** Fork and duplicate share one rate-limit budget (same "clone" operation class) */ const { forkIpLimiter, forkUserLimiter } = createForkLimiters(); -const upload = multer({ storage: storage, fileFilter: importFileFilter }); +const importMaxFileSize = resolveImportMaxFileSize(); +const upload = multer({ + storage, + fileFilter: importFileFilter, + limits: { fileSize: importMaxFileSize }, +}); +const uploadSingle = upload.single('file'); + +function handleUpload(req, res, next) { + uploadSingle(req, res, (err) => { + if (err && err.code === 'LIMIT_FILE_SIZE') { + return res.status(413).json({ message: 'File exceeds the maximum allowed size' }); + } + if (err) { + return next(err); + } + next(); + }); +} /** * Imports a conversation from a JSON file and saves it to the database. @@ -239,7 +257,7 @@ router.post( importIpLimiter, importUserLimiter, configMiddleware, - upload.single('file'), + handleUpload, async (req, res) => { try { /* TODO: optimize to return imported conversations and add manually */ diff --git a/api/server/utils/import/importConversations.js b/api/server/utils/import/importConversations.js index d9e4d4332d..e56176c609 100644 --- a/api/server/utils/import/importConversations.js +++ b/api/server/utils/import/importConversations.js @@ -1,7 +1,10 @@ const fs = require('fs').promises; +const { resolveImportMaxFileSize } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { getImporter } = require('./importers'); +const maxFileSize = resolveImportMaxFileSize(); + /** * Job definition for importing a conversation. * @param {{ filepath, requestUserId }} job - The job object. @@ -11,11 +14,10 @@ const importConversations = async (job) => { try { logger.debug(`user: ${requestUserId} | Importing conversation(s) from file...`); - /* error if file is too large */ const fileInfo = await fs.stat(filepath); - if (fileInfo.size > process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES) { + if (fileInfo.size > maxFileSize) { throw new Error( - `File size is ${fileInfo.size} bytes. It exceeds the maximum limit of ${process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES} bytes.`, + `File size is ${fileInfo.size} bytes. It exceeds the maximum limit of ${maxFileSize} bytes.`, ); } diff --git a/packages/api/src/utils/__tests__/import.test.ts b/packages/api/src/utils/__tests__/import.test.ts new file mode 100644 index 0000000000..08fa94669d --- /dev/null +++ b/packages/api/src/utils/__tests__/import.test.ts @@ -0,0 +1,76 @@ +jest.mock('@librechat/data-schemas', () => ({ + logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, +})); + +import { DEFAULT_IMPORT_MAX_FILE_SIZE, resolveImportMaxFileSize } from '../import'; +import { logger } from '@librechat/data-schemas'; + +describe('resolveImportMaxFileSize', () => { + let originalEnv: string | undefined; + + beforeEach(() => { + originalEnv = process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + jest.clearAllMocks(); + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = originalEnv; + } else { + delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + } + }); + + it('returns 262144000 (250 MiB) when env var is not set', () => { + delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + expect(resolveImportMaxFileSize()).toBe(262144000); + expect(DEFAULT_IMPORT_MAX_FILE_SIZE).toBe(262144000); + }); + + it('returns default when env var is empty string', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = ''; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + }); + + it('respects a custom numeric value', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '5242880'; + expect(resolveImportMaxFileSize()).toBe(5242880); + }); + + it('parses string env var to number', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '1048576'; + expect(resolveImportMaxFileSize()).toBe(1048576); + }); + + it('falls back to default and warns for non-numeric string', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = 'abc'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); + + it('falls back to default and warns for negative values', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '-100'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); + + it('falls back to default and warns for zero', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '0'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); + + it('falls back to default and warns for Infinity', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = 'Infinity'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); +}); diff --git a/packages/api/src/utils/import.ts b/packages/api/src/utils/import.ts new file mode 100644 index 0000000000..94a2c8f818 --- /dev/null +++ b/packages/api/src/utils/import.ts @@ -0,0 +1,20 @@ +import { logger } from '@librechat/data-schemas'; + +/** 250 MiB — default max file size for conversation imports */ +export const DEFAULT_IMPORT_MAX_FILE_SIZE = 262144000; + +/** Resolves the import file-size limit from the env var, falling back to the 250 MiB default */ +export function resolveImportMaxFileSize(): number { + const raw = process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + if (!raw) { + return DEFAULT_IMPORT_MAX_FILE_SIZE; + } + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) { + logger.warn( + `[imports] Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES="${raw}"; using default ${DEFAULT_IMPORT_MAX_FILE_SIZE}`, + ); + return DEFAULT_IMPORT_MAX_FILE_SIZE; + } + return parsed; +} diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 441c2e02d7..5b9315d8c7 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -6,6 +6,7 @@ export * from './email'; export * from './env'; export * from './events'; export * from './files'; +export * from './import'; export * from './generators'; export * from './graph'; export * from './path'; From f67bbb2bc5e1722c91a106f166629a026c6f0d6a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 03:09:26 -0400 Subject: [PATCH 030/111] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20Sanitize=20Artifa?= =?UTF-8?q?ct=20Filenames=20in=20Code=20Execution=20Output=20(#12222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: sanitize artifact filenames to prevent path traversal in code output * test: Mock sanitizeFilename function in process.spec.js to return the original filename - Added a mock implementation for the `sanitizeFilename` function in the `process.spec.js` test file to return the original filename, ensuring that tests can run without altering the filename during the testing process. * fix: use path.relative for traversal check, sanitize all filenames, add security logging - Replace startsWith with path.relative pattern in saveLocalBuffer, consistent with deleteLocalFile and getLocalFileStream in the same file - Hoist sanitizeFilename call before the image/non-image branch so both code paths store the sanitized name in MongoDB - Log a warning when sanitizeFilename mutates a filename (potential traversal) - Log a specific warning when saveLocalBuffer throws a traversal error, so security events are distinguishable from generic network errors in the catch * test: improve traversal test coverage and remove mock reimplementation - Remove partial sanitizeFilename reimplementation from process-traversal tests; use controlled mock returns to verify processCodeOutput wiring instead - Add test for image branch sanitization - Use mkdtempSync for test isolation in crud-traversal to avoid parallel worker collisions - Add prefix-collision bypass test case (../user10/evil vs user1 directory) * fix: use path.relative in isValidPath to prevent prefix-collision bypass Pre-existing startsWith check without path separator had the same class of prefix-collision vulnerability fixed in saveLocalBuffer. --- .../Code/__tests__/process-traversal.spec.js | 124 ++++++++++++++++++ api/server/services/Files/Code/process.js | 20 ++- .../services/Files/Code/process.spec.js | 1 + .../Local/__tests__/crud-traversal.spec.js | 69 ++++++++++ api/server/services/Files/Local/crud.js | 16 ++- 5 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 api/server/services/Files/Code/__tests__/process-traversal.spec.js create mode 100644 api/server/services/Files/Local/__tests__/crud-traversal.spec.js diff --git a/api/server/services/Files/Code/__tests__/process-traversal.spec.js b/api/server/services/Files/Code/__tests__/process-traversal.spec.js new file mode 100644 index 0000000000..2db366d06b --- /dev/null +++ b/api/server/services/Files/Code/__tests__/process-traversal.spec.js @@ -0,0 +1,124 @@ +jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') })); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { warn: jest.fn(), debug: jest.fn(), error: jest.fn() }, +})); + +jest.mock('@librechat/agents', () => ({ + getCodeBaseURL: jest.fn(() => 'http://localhost:8000'), +})); + +const mockSanitizeFilename = jest.fn(); + +jest.mock('@librechat/api', () => ({ + logAxiosError: jest.fn(), + getBasePath: jest.fn(() => ''), + sanitizeFilename: mockSanitizeFilename, +})); + +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), + mergeFileConfig: jest.fn(() => ({ serverFileSizeLimit: 100 * 1024 * 1024 })), + getEndpointFileConfig: jest.fn(() => ({ + fileSizeLimit: 100 * 1024 * 1024, + supportedMimeTypes: ['*/*'], + })), + fileConfig: { checkType: jest.fn(() => true) }, +})); + +jest.mock('~/models', () => ({ + createFile: jest.fn().mockResolvedValue({}), + getFiles: jest.fn().mockResolvedValue([]), + updateFile: jest.fn(), + claimCodeFile: jest.fn().mockResolvedValue({ file_id: 'mock-uuid', usage: 0 }), +})); + +const mockSaveBuffer = jest.fn().mockResolvedValue('/uploads/user123/mock-uuid__output.csv'); + +jest.mock('~/server/services/Files/strategies', () => ({ + getStrategyFunctions: jest.fn(() => ({ + saveBuffer: mockSaveBuffer, + })), +})); + +jest.mock('~/server/services/Files/permissions', () => ({ + filterFilesByAgentAccess: jest.fn().mockResolvedValue([]), +})); + +jest.mock('~/server/services/Files/images/convert', () => ({ + convertImage: jest.fn(), +})); + +jest.mock('~/server/utils', () => ({ + determineFileType: jest.fn().mockResolvedValue({ mime: 'text/csv' }), +})); + +jest.mock('axios', () => + jest.fn().mockResolvedValue({ + data: Buffer.from('file-content'), + }), +); + +const { createFile } = require('~/models'); +const { processCodeOutput } = require('../process'); + +const baseParams = { + req: { + user: { id: 'user123' }, + config: { + fileStrategy: 'local', + imageOutputType: 'webp', + fileConfig: {}, + }, + }, + id: 'code-file-id', + apiKey: 'test-key', + toolCallId: 'tool-1', + conversationId: 'conv-1', + messageId: 'msg-1', + session_id: 'session-1', +}; + +describe('processCodeOutput path traversal protection', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('sanitizeFilename is called with the raw artifact name', async () => { + mockSanitizeFilename.mockReturnValueOnce('output.csv'); + await processCodeOutput({ ...baseParams, name: 'output.csv' }); + expect(mockSanitizeFilename).toHaveBeenCalledWith('output.csv'); + }); + + test('sanitized name is used in saveBuffer fileName', async () => { + mockSanitizeFilename.mockReturnValueOnce('sanitized-name.txt'); + await processCodeOutput({ ...baseParams, name: '../../../tmp/poc.txt' }); + + expect(mockSanitizeFilename).toHaveBeenCalledWith('../../../tmp/poc.txt'); + const call = mockSaveBuffer.mock.calls[0][0]; + expect(call.fileName).toBe('mock-uuid__sanitized-name.txt'); + }); + + test('sanitized name is stored as filename in the file record', async () => { + mockSanitizeFilename.mockReturnValueOnce('safe-output.csv'); + await processCodeOutput({ ...baseParams, name: 'unsafe/../../output.csv' }); + + const fileArg = createFile.mock.calls[0][0]; + expect(fileArg.filename).toBe('safe-output.csv'); + }); + + test('sanitized name is used for image file records', async () => { + const { convertImage } = require('~/server/services/Files/images/convert'); + convertImage.mockResolvedValueOnce({ + filepath: '/images/user123/mock-uuid.webp', + bytes: 100, + }); + + mockSanitizeFilename.mockReturnValueOnce('safe-chart.png'); + await processCodeOutput({ ...baseParams, name: '../../../chart.png' }); + + expect(mockSanitizeFilename).toHaveBeenCalledWith('../../../chart.png'); + const fileArg = createFile.mock.calls[0][0]; + expect(fileArg.filename).toBe('safe-chart.png'); + }); +}); diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js index 3f0bfcfc87..e878b00255 100644 --- a/api/server/services/Files/Code/process.js +++ b/api/server/services/Files/Code/process.js @@ -3,7 +3,7 @@ const { v4 } = require('uuid'); const axios = require('axios'); const { logger } = require('@librechat/data-schemas'); const { getCodeBaseURL } = require('@librechat/agents'); -const { logAxiosError, getBasePath } = require('@librechat/api'); +const { logAxiosError, getBasePath, sanitizeFilename } = require('@librechat/api'); const { Tools, megabyte, @@ -146,6 +146,13 @@ const processCodeOutput = async ({ ); } + const safeName = sanitizeFilename(name); + if (safeName !== name) { + logger.warn( + `[processCodeOutput] Filename sanitized: "${name}" -> "${safeName}" | conv=${conversationId}`, + ); + } + if (isImage) { const usage = isUpdate ? (claimed.usage ?? 0) + 1 : 1; const _file = await convertImage(req, buffer, 'high', `${file_id}${fileExt}`); @@ -156,7 +163,7 @@ const processCodeOutput = async ({ file_id, messageId, usage, - filename: name, + filename: safeName, conversationId, user: req.user.id, type: `image/${appConfig.imageOutputType}`, @@ -200,7 +207,7 @@ const processCodeOutput = async ({ ); } - const fileName = `${file_id}__${name}`; + const fileName = `${file_id}__${safeName}`; const filepath = await saveBuffer({ userId: req.user.id, buffer, @@ -213,7 +220,7 @@ const processCodeOutput = async ({ filepath, messageId, object: 'file', - filename: name, + filename: safeName, type: mimeType, conversationId, user: req.user.id, @@ -229,6 +236,11 @@ const processCodeOutput = async ({ await createFile(file, true); return Object.assign(file, { messageId, toolCallId }); } catch (error) { + if (error?.message === 'Path traversal detected in filename') { + logger.warn( + `[processCodeOutput] Path traversal blocked for file "${name}" | conv=${conversationId}`, + ); + } logAxiosError({ message: 'Error downloading/processing code environment file', error, diff --git a/api/server/services/Files/Code/process.spec.js b/api/server/services/Files/Code/process.spec.js index f01a623f90..b89a6c6307 100644 --- a/api/server/services/Files/Code/process.spec.js +++ b/api/server/services/Files/Code/process.spec.js @@ -58,6 +58,7 @@ jest.mock('@librechat/agents', () => ({ jest.mock('@librechat/api', () => ({ logAxiosError: jest.fn(), getBasePath: jest.fn(() => ''), + sanitizeFilename: jest.fn((name) => name), })); // Mock models diff --git a/api/server/services/Files/Local/__tests__/crud-traversal.spec.js b/api/server/services/Files/Local/__tests__/crud-traversal.spec.js new file mode 100644 index 0000000000..57ba221d68 --- /dev/null +++ b/api/server/services/Files/Local/__tests__/crud-traversal.spec.js @@ -0,0 +1,69 @@ +jest.mock('@librechat/api', () => ({ deleteRagFile: jest.fn() })); +jest.mock('@librechat/data-schemas', () => ({ + logger: { warn: jest.fn(), error: jest.fn() }, +})); + +const mockTmpBase = require('fs').mkdtempSync( + require('path').join(require('os').tmpdir(), 'crud-traversal-'), +); + +jest.mock('~/config/paths', () => { + const path = require('path'); + return { + publicPath: path.join(mockTmpBase, 'public'), + uploads: path.join(mockTmpBase, 'uploads'), + }; +}); + +const fs = require('fs'); +const path = require('path'); +const { saveLocalBuffer } = require('../crud'); + +describe('saveLocalBuffer path containment', () => { + beforeAll(() => { + fs.mkdirSync(path.join(mockTmpBase, 'public', 'images'), { recursive: true }); + fs.mkdirSync(path.join(mockTmpBase, 'uploads'), { recursive: true }); + }); + + afterAll(() => { + fs.rmSync(mockTmpBase, { recursive: true, force: true }); + }); + + test('rejects filenames with path traversal sequences', async () => { + await expect( + saveLocalBuffer({ + userId: 'user1', + buffer: Buffer.from('malicious'), + fileName: '../../../etc/passwd', + basePath: 'uploads', + }), + ).rejects.toThrow('Path traversal detected in filename'); + }); + + test('rejects prefix-collision traversal (startsWith bypass)', async () => { + fs.mkdirSync(path.join(mockTmpBase, 'uploads', 'user10'), { recursive: true }); + await expect( + saveLocalBuffer({ + userId: 'user1', + buffer: Buffer.from('malicious'), + fileName: '../user10/evil', + basePath: 'uploads', + }), + ).rejects.toThrow('Path traversal detected in filename'); + }); + + test('allows normal filenames', async () => { + const result = await saveLocalBuffer({ + userId: 'user1', + buffer: Buffer.from('safe content'), + fileName: 'file-id__output.csv', + basePath: 'uploads', + }); + + expect(result).toBe('/uploads/user1/file-id__output.csv'); + + const filePath = path.join(mockTmpBase, 'uploads', 'user1', 'file-id__output.csv'); + expect(fs.existsSync(filePath)).toBe(true); + fs.unlinkSync(filePath); + }); +}); diff --git a/api/server/services/Files/Local/crud.js b/api/server/services/Files/Local/crud.js index 1f38a01f83..c86774d472 100644 --- a/api/server/services/Files/Local/crud.js +++ b/api/server/services/Files/Local/crud.js @@ -78,7 +78,13 @@ async function saveLocalBuffer({ userId, buffer, fileName, basePath = 'images' } fs.mkdirSync(directoryPath, { recursive: true }); } - fs.writeFileSync(path.join(directoryPath, fileName), buffer); + const resolvedDir = path.resolve(directoryPath); + const resolvedPath = path.resolve(resolvedDir, fileName); + const rel = path.relative(resolvedDir, resolvedPath); + if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { + throw new Error('Path traversal detected in filename'); + } + fs.writeFileSync(resolvedPath, buffer); const filePath = path.posix.join('/', basePath, userId, fileName); @@ -165,9 +171,8 @@ async function getLocalFileURL({ fileName, basePath = 'images' }) { } /** - * Validates if a given filepath is within a specified subdirectory under a base path. This function constructs - * the expected base path using the base, subfolder, and user id from the request, and then checks if the - * provided filepath starts with this constructed base path. + * Validates that a filepath is strictly contained within a subdirectory under a base path, + * using path.relative to prevent prefix-collision bypasses. * * @param {ServerRequest} req - The request object from Express. It should contain a `user` property with an `id`. * @param {string} base - The base directory path. @@ -180,7 +185,8 @@ async function getLocalFileURL({ fileName, basePath = 'images' }) { const isValidPath = (req, base, subfolder, filepath) => { const normalizedBase = path.resolve(base, subfolder, req.user.id); const normalizedFilepath = path.resolve(filepath); - return normalizedFilepath.startsWith(normalizedBase); + const rel = path.relative(normalizedBase, normalizedFilepath); + return !rel.startsWith('..') && !path.isAbsolute(rel) && !rel.includes(`..${path.sep}`); }; /** From cbdc6f606057296aa2a92eaf7636347757432849 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 03:36:03 -0400 Subject: [PATCH 031/111] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20NPM=20Au?= =?UTF-8?q?dit=20Packages=20(#12227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 chore: Update file-type dependency to version 21.3.2 in package-lock.json and package.json - Upgraded the "file-type" package from version 18.7.0 to 21.3.2 to ensure compatibility with the latest features and security updates. - Added new dependencies related to the updated "file-type" package, enhancing functionality and performance. * 🔧 chore: Upgrade undici dependency to version 7.24.1 in package-lock.json and package.json - Updated the "undici" package from version 7.18.2 to 7.24.1 across multiple package files to ensure compatibility with the latest features and security updates. * 🔧 chore: Upgrade yauzl dependency to version 3.2.1 in package-lock.json - Updated the "yauzl" package from version 3.2.0 to 3.2.1 to incorporate the latest features and security updates. * 🔧 chore: Upgrade hono dependency to version 4.12.7 in package-lock.json - Updated the "hono" package from version 4.12.5 to 4.12.7 to incorporate the latest features and security updates. --- api/package.json | 4 +- package-lock.json | 208 ++++++++++++++++++++++---------------- packages/api/package.json | 2 +- 3 files changed, 124 insertions(+), 90 deletions(-) diff --git a/api/package.json b/api/package.json index 1618481b58..0305446818 100644 --- a/api/package.json +++ b/api/package.json @@ -67,7 +67,7 @@ "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", - "file-type": "^18.7.0", + "file-type": "^21.3.2", "firebase": "^11.0.2", "form-data": "^4.0.4", "handlebars": "^4.7.7", @@ -109,7 +109,7 @@ "sharp": "^0.33.5", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "undici": "^7.18.2", + "undici": "^7.24.1", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", diff --git a/package-lock.json b/package-lock.json index a2db2df389..502b3a8eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,7 @@ "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", - "file-type": "^18.7.0", + "file-type": "^21.3.2", "firebase": "^11.0.2", "form-data": "^4.0.4", "handlebars": "^4.7.7", @@ -124,7 +124,7 @@ "sharp": "^0.33.5", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "undici": "^7.18.2", + "undici": "^7.24.1", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", @@ -270,6 +270,24 @@ "node": ">= 0.8.0" } }, + "api/node_modules/file-type": { + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "api/node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -348,6 +366,40 @@ "@img/sharp-win32-x64": "0.33.5" } }, + "api/node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "api/node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "api/node_modules/winston-daily-rotate-file": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", @@ -7286,6 +7338,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", @@ -20799,6 +20861,41 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -27513,22 +27610,6 @@ "moment": "^2.29.1" } }, - "node_modules/file-type": { - "version": "18.7.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", - "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -28817,9 +28898,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/hono": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -35702,18 +35783,6 @@ "node-readable-to-web-readable-stream": "^0.4.2" } }, - "node_modules/peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -38519,21 +38588,6 @@ "node": ">= 6" } }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -40920,22 +40974,6 @@ ], "license": "MIT" }, - "node_modules/strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/style-inject": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", @@ -41640,22 +41678,6 @@ "node": ">=0.6" } }, - "node_modules/token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -42206,6 +42228,18 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -42238,9 +42272,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.20.0.tgz", - "integrity": "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", + "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -44097,9 +44131,9 @@ } }, "node_modules/yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz", + "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==", "dev": true, "license": "MIT", "dependencies": { @@ -44232,7 +44266,7 @@ "node-fetch": "2.7.0", "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", - "undici": "^7.18.2", + "undici": "^7.24.1", "zod": "^3.22.4" } }, diff --git a/packages/api/package.json b/packages/api/package.json index 966447c51b..77258fc0b3 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -117,7 +117,7 @@ "node-fetch": "2.7.0", "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", - "undici": "^7.18.2", + "undici": "^7.24.1", "zod": "^3.22.4" } } From 7bc793b18db312feab9ad15e765f2f2da34d0667 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 10:54:26 -0400 Subject: [PATCH 032/111] =?UTF-8?q?=F0=9F=8C=8A=20fix:=20Prevent=20Buffere?= =?UTF-8?q?d=20Event=20Duplication=20on=20SSE=20Resume=20Connections=20(#1?= =?UTF-8?q?2225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: skipBufferReplay for job resume connections - Introduced a new option `skipBufferReplay` in the `subscribe` method of `GenerationJobManagerClass` to prevent duplication of events when resuming a connection. - Updated the logic to conditionally skip replaying buffered events if a sync event has already been sent, enhancing the efficiency of event handling during reconnections. - Added integration tests to verify the correct behavior of the new option, ensuring that no buffered events are replayed when `skipBufferReplay` is true, while still allowing for normal replay behavior when false. * refactor: Update GenerationJobManager to handle sync events more efficiently - Modified the `subscribe` method to utilize a new `skipBufferReplay` option, allowing for the prevention of duplicate events during resume connections. - Enhanced the logic in the `chat/stream` route to conditionally skip replaying buffered events if a sync event has already been sent, improving event handling efficiency. - Updated integration tests to verify the correct behavior of the new option, ensuring that no buffered events are replayed when `skipBufferReplay` is true, while maintaining normal replay behavior when false. * test: Enhance GenerationJobManager integration tests for Redis mode - Updated integration tests to conditionally run based on the USE_REDIS environment variable, allowing for better control over Redis-related tests. - Refactored test descriptions to utilize a dynamic `describeRedis` function, improving clarity and organization of tests related to Redis functionality. - Removed redundant checks for Redis availability within individual tests, streamlining the test logic and enhancing readability. * fix: sync handler state for new messages on resume The sync event's else branch (new response message) was missing resetContentHandler() and syncStepMessage() calls, leaving stale handler state that caused subsequent deltas to build on partial content instead of the synced aggregatedContent. * feat: atomic subscribeWithResume to close resume event gap Replaces separate getResumeState() + subscribe() calls with a single subscribeWithResume() that atomically drains earlyEventBuffer between the resume snapshot and the subscribe. In in-memory mode, drained events are returned as pendingEvents for the client to replay after sync. In Redis mode, pendingEvents is empty since chunks are already persisted. The route handler now uses the atomic method for resume connections and extracted shared SSE write helpers to reduce duplication. The client replays any pendingEvents through the existing step/content handlers after applying aggregatedContent from the sync payload. * fix: only capture gap events in subscribeWithResume, not pre-snapshot buffer The previous implementation drained the entire earlyEventBuffer into pendingEvents, but pre-snapshot events are already reflected in aggregatedContent. Replaying them re-introduced the duplication bug through a different vector. Now records buffer length before getResumeState() and slices from that index, so only events arriving during the async gap are returned as pendingEvents. Also: - Handle pendingEvents when resumeState is null (replay directly) - Hoist duplicate test helpers to shared scope - Remove redundant writableEnded guard in onDone --- api/server/routes/agents/index.js | 82 +- client/src/hooks/SSE/useResumableSSE.ts | 44 +- .../api/src/stream/GenerationJobManager.ts | 67 +- ...ationJobManager.stream_integration.spec.ts | 745 +++++++++++++----- packages/api/src/types/stream.ts | 21 + 5 files changed, 700 insertions(+), 259 deletions(-) diff --git a/api/server/routes/agents/index.js b/api/server/routes/agents/index.js index f8d39cb4d8..a99fdca592 100644 --- a/api/server/routes/agents/index.js +++ b/api/server/routes/agents/index.js @@ -76,52 +76,62 @@ router.get('/chat/stream/:streamId', async (req, res) => { logger.debug(`[AgentStream] Client subscribed to ${streamId}, resume: ${isResume}`); - // Send sync event with resume state for ALL reconnecting clients - // This supports multi-tab scenarios where each tab needs run step data - if (isResume) { - const resumeState = await GenerationJobManager.getResumeState(streamId); - if (resumeState && !res.writableEnded) { - // Send sync event with run steps AND aggregatedContent - // Client will use aggregatedContent to initialize message state - res.write(`event: message\ndata: ${JSON.stringify({ sync: true, resumeState })}\n\n`); + const writeEvent = (event) => { + if (!res.writableEnded) { + res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`); if (typeof res.flush === 'function') { res.flush(); } - logger.debug( - `[AgentStream] Sent sync event for ${streamId} with ${resumeState.runSteps.length} run steps`, - ); } - } + }; - const result = await GenerationJobManager.subscribe( - streamId, - (event) => { - if (!res.writableEnded) { - res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`); + const onDone = (event) => { + writeEvent(event); + res.end(); + }; + + const onError = (error) => { + if (!res.writableEnded) { + res.write(`event: error\ndata: ${JSON.stringify({ error })}\n\n`); + if (typeof res.flush === 'function') { + res.flush(); + } + res.end(); + } + }; + + let result; + + if (isResume) { + const { subscription, resumeState, pendingEvents } = + await GenerationJobManager.subscribeWithResume(streamId, writeEvent, onDone, onError); + + if (!res.writableEnded) { + if (resumeState) { + res.write( + `event: message\ndata: ${JSON.stringify({ sync: true, resumeState, pendingEvents })}\n\n`, + ); if (typeof res.flush === 'function') { res.flush(); } - } - }, - (event) => { - if (!res.writableEnded) { - res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`); - if (typeof res.flush === 'function') { - res.flush(); + GenerationJobManager.markSyncSent(streamId); + logger.debug( + `[AgentStream] Sent sync event for ${streamId} with ${resumeState.runSteps.length} run steps, ${pendingEvents.length} pending events`, + ); + } else if (pendingEvents.length > 0) { + for (const event of pendingEvents) { + writeEvent(event); } - res.end(); + logger.warn( + `[AgentStream] Resume state null for ${streamId}, replayed ${pendingEvents.length} gap events directly`, + ); } - }, - (error) => { - if (!res.writableEnded) { - res.write(`event: error\ndata: ${JSON.stringify({ error })}\n\n`); - if (typeof res.flush === 'function') { - res.flush(); - } - res.end(); - } - }, - ); + } + + result = subscription; + } else { + result = await GenerationJobManager.subscribe(streamId, writeEvent, onDone, onError); + } if (!result) { return res.status(404).json({ error: 'Failed to subscribe to stream' }); diff --git a/client/src/hooks/SSE/useResumableSSE.ts b/client/src/hooks/SSE/useResumableSSE.ts index 831bf042ad..4d4cb4841a 100644 --- a/client/src/hooks/SSE/useResumableSSE.ts +++ b/client/src/hooks/SSE/useResumableSSE.ts @@ -226,12 +226,12 @@ export default function useResumableSSE( if (data.sync != null) { console.log('[ResumableSSE] SYNC received', { runSteps: data.resumeState?.runSteps?.length ?? 0, + pendingEvents: data.pendingEvents?.length ?? 0, }); const runId = v4(); setActiveRunId(runId); - // Replay run steps if (data.resumeState?.runSteps) { for (const runStep of data.resumeState.runSteps) { stepHandler({ event: 'on_run_step', data: runStep }, { @@ -241,19 +241,15 @@ export default function useResumableSSE( } } - // Set message content from aggregatedContent if (data.resumeState?.aggregatedContent && userMessage?.messageId) { const messages = getMessages() ?? []; const userMsgId = userMessage.messageId; const serverResponseId = data.resumeState.responseMessageId; - // Find the EXACT response message - prioritize responseMessageId from server - // This is critical when there are multiple responses to the same user message let responseIdx = -1; if (serverResponseId) { responseIdx = messages.findIndex((m) => m.messageId === serverResponseId); } - // Fallback: find by parentMessageId pattern (for new messages) if (responseIdx < 0) { responseIdx = messages.findIndex( (m) => @@ -272,7 +268,6 @@ export default function useResumableSSE( }); if (responseIdx >= 0) { - // Update existing response message with aggregatedContent const updated = [...messages]; const oldContent = updated[responseIdx]?.content; updated[responseIdx] = { @@ -285,25 +280,34 @@ export default function useResumableSSE( newContentLength: data.resumeState.aggregatedContent?.length, }); setMessages(updated); - // Sync both content handler and step handler with the updated message - // so subsequent deltas build on synced content, not stale content resetContentHandler(); syncStepMessage(updated[responseIdx]); console.log('[ResumableSSE] SYNC complete, handlers synced'); } else { - // Add new response message const responseId = serverResponseId ?? `${userMsgId}_`; - setMessages([ - ...messages, - { - messageId: responseId, - parentMessageId: userMsgId, - conversationId: currentSubmission.conversation?.conversationId ?? '', - text: '', - content: data.resumeState.aggregatedContent, - isCreatedByUser: false, - } as TMessage, - ]); + const newMessage = { + messageId: responseId, + parentMessageId: userMsgId, + conversationId: currentSubmission.conversation?.conversationId ?? '', + text: '', + content: data.resumeState.aggregatedContent, + isCreatedByUser: false, + } as TMessage; + setMessages([...messages, newMessage]); + resetContentHandler(); + syncStepMessage(newMessage); + } + } + + if (data.pendingEvents?.length > 0) { + console.log(`[ResumableSSE] Replaying ${data.pendingEvents.length} pending events`); + const submission = { ...currentSubmission, userMessage } as EventSubmission; + for (const pendingEvent of data.pendingEvents) { + if (pendingEvent.event != null) { + stepHandler(pendingEvent, submission); + } else if (pendingEvent.type != null) { + contentHandler({ data: pendingEvent, submission }); + } } } diff --git a/packages/api/src/stream/GenerationJobManager.ts b/packages/api/src/stream/GenerationJobManager.ts index cd5ff04eb0..1b612dcb8f 100644 --- a/packages/api/src/stream/GenerationJobManager.ts +++ b/packages/api/src/stream/GenerationJobManager.ts @@ -707,6 +707,10 @@ class GenerationJobManagerClass { * @param onChunk - Handler for chunk events (streamed tokens, run steps, etc.) * @param onDone - Handler for completion event (includes final message) * @param onError - Handler for error events + * @param options - Subscription configuration + * @param options.skipBufferReplay - When true, skips replaying the earlyEventBuffer. + * Use this when a sync event was already sent (resume), since the sync's + * aggregatedContent already includes all buffered events. * @returns Subscription object with unsubscribe function, or null if job not found */ async subscribe( @@ -714,6 +718,7 @@ class GenerationJobManagerClass { onChunk: t.ChunkHandler, onDone?: t.DoneHandler, onError?: t.ErrorHandler, + options?: t.SubscribeOptions, ): Promise<{ unsubscribe: t.UnsubscribeFn } | null> { // Use lazy initialization to support cross-replica subscriptions const runtime = await this.getOrCreateRuntimeState(streamId); @@ -763,11 +768,17 @@ class GenerationJobManagerClass { runtime.hasSubscriber = true; if (runtime.earlyEventBuffer.length > 0) { - logger.debug( - `[GenerationJobManager] Replaying ${runtime.earlyEventBuffer.length} buffered events for ${streamId}`, - ); - for (const bufferedEvent of runtime.earlyEventBuffer) { - onChunk(bufferedEvent); + if (options?.skipBufferReplay) { + logger.debug( + `[GenerationJobManager] Skipping ${runtime.earlyEventBuffer.length} buffered events for ${streamId} (skipBufferReplay)`, + ); + } else { + logger.debug( + `[GenerationJobManager] Replaying ${runtime.earlyEventBuffer.length} buffered events for ${streamId}`, + ); + for (const bufferedEvent of runtime.earlyEventBuffer) { + onChunk(bufferedEvent); + } } runtime.earlyEventBuffer = []; } @@ -785,6 +796,52 @@ class GenerationJobManagerClass { return subscription; } + /** + * Atomic resume + subscribe: snapshots resume state and drains the early event buffer + * in one synchronous step, then subscribes with skipBufferReplay. + * + * Closes the timing gap between separate `getResumeState()` and `subscribe()` calls + * where events could arrive in earlyEventBuffer after the snapshot but before subscribe + * clears the buffer. + * + * In-memory mode: drained buffer events are returned as `pendingEvents` since + * they exist nowhere else. The caller must deliver them after the sync payload. + * Redis mode: `pendingEvents` is empty — chunks are persisted via appendChunk + * and will appear in aggregatedContent on the next resume. + */ + async subscribeWithResume( + streamId: string, + onChunk: t.ChunkHandler, + onDone?: t.DoneHandler, + onError?: t.ErrorHandler, + ): Promise { + const bufferLengthAtSnapshot = !this._isRedis + ? (this.runtimeState.get(streamId)?.earlyEventBuffer.length ?? 0) + : 0; + + const resumeState = await this.getResumeState(streamId); + + let pendingEvents: t.ServerSentEvent[] = []; + if (!this._isRedis) { + const runtime = this.runtimeState.get(streamId); + if (runtime) { + pendingEvents = runtime.earlyEventBuffer.slice(bufferLengthAtSnapshot); + runtime.earlyEventBuffer = []; + if (pendingEvents.length > 0) { + logger.debug( + `[GenerationJobManager] Captured ${pendingEvents.length} gap events for ${streamId}`, + ); + } + } + } + + const subscription = await this.subscribe(streamId, onChunk, onDone, onError, { + skipBufferReplay: true, + }); + + return { subscription, resumeState, pendingEvents }; + } + /** * Emit a chunk event to all subscribers. * Uses runtime state check for performance (avoids async job store lookup per token). diff --git a/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts b/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts index 59fe32e4e5..2f23510018 100644 --- a/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts +++ b/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts @@ -1,3 +1,4 @@ +/* eslint jest/no-standalone-expect: ["error", { "additionalTestBlockFunctions": ["testRedis"] }] */ import type { Redis, Cluster } from 'ioredis'; import type { ServerSentEvent } from '~/types/events'; import { InMemoryEventTransport } from '~/stream/implementations/InMemoryEventTransport'; @@ -27,6 +28,9 @@ describe('GenerationJobManager Integration Tests', () => { let dynamicKeyvClient: unknown = null; let dynamicKeyvReady: Promise | null = null; const testPrefix = 'JobManager-Integration-Test'; + const redisConfigured = process.env.USE_REDIS === 'true'; + const describeRedis = redisConfigured ? describe : describe.skip; + const testRedis = redisConfigured ? test : test.skip; beforeAll(async () => { originalEnv = { ...process.env }; @@ -82,6 +86,68 @@ describe('GenerationJobManager Integration Tests', () => { process.env = originalEnv; }); + function createInMemoryManager(): GenerationJobManagerClass { + const manager = new GenerationJobManagerClass(); + manager.configure({ + jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), + eventTransport: new InMemoryEventTransport(), + isRedis: false, + }); + manager.initialize(); + return manager; + } + + function createRedisManager(): GenerationJobManagerClass { + const manager = new GenerationJobManagerClass(); + manager.configure( + createStreamServices({ + useRedis: true, + redisClient: ioredisClient!, + }), + ); + manager.initialize(); + return manager; + } + + async function setupDisconnectedStream( + manager: GenerationJobManagerClass, + streamId: string, + delay: number, + ): Promise { + const firstEvents: ServerSentEvent[] = []; + const sub = await manager.subscribe(streamId, (event) => firstEvents.push(event)); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + await manager.emitChunk(streamId, { + event: 'on_run_step', + data: { id: 'step-1', runId: 'run-1', index: 0, stepDetails: { type: 'message_creation' } }, + }); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: 'Hello' } } }, + }); + + await new Promise((resolve) => setTimeout(resolve, delay)); + expect(firstEvents.length).toBe(2); + + sub?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, delay)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: ' world' } } }, + }); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: '!' } } }, + }); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + return firstEvents; + } + describe('In-Memory Mode', () => { test('should create and manage jobs', async () => { // Configure with in-memory @@ -171,13 +237,8 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('Redis Mode', () => { + describeRedis('Redis Mode', () => { test('should create and manage jobs via Redis', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - // Create Redis services const services = createStreamServices({ useRedis: true, @@ -209,11 +270,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should persist chunks for cross-instance resume', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, @@ -264,11 +320,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should handle abort and return content', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, @@ -374,7 +425,7 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('Cross-Replica Support (Redis)', () => { + describeRedis('Cross-Replica Support (Redis)', () => { /** * Problem: In k8s with Redis and multiple replicas, when a user sends a message: * 1. POST /api/agents/chat hits Replica A, creates job @@ -387,15 +438,10 @@ describe('GenerationJobManager Integration Tests', () => { * when the job exists in Redis but not in local memory. */ test('should NOT return 404 when stream endpoint hits different replica than job creator', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - // === REPLICA A: Creates the job === // Simulate Replica A creating the job directly in Redis // (In real scenario, this happens via GenerationJobManager.createJob on Replica A) - const replicaAJobStore = new RedisJobStore(ioredisClient); + const replicaAJobStore = new RedisJobStore(ioredisClient!); await replicaAJobStore.initialize(); const streamId = `cross-replica-404-test-${Date.now()}`; @@ -452,13 +498,8 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should lazily create runtime state for jobs created on other replicas', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - // Instance 1: Create the job directly in Redis (simulating another replica) - const jobStore = new RedisJobStore(ioredisClient); + const jobStore = new RedisJobStore(ioredisClient!); await jobStore.initialize(); const streamId = `cross-replica-${Date.now()}`; @@ -500,11 +541,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should persist syncSent to Redis for cross-replica consistency', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, @@ -539,11 +575,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should persist finalEvent to Redis for cross-replica access', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, @@ -581,11 +612,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should emit cross-replica abort signal via Redis pub/sub', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, @@ -620,16 +646,11 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should handle abort for lazily-initialized cross-replica jobs', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - // This test validates that jobs created on Replica A and lazily-initialized // on Replica B can still receive and handle abort signals. // === Replica A: Create job directly in Redis === - const replicaAJobStore = new RedisJobStore(ioredisClient); + const replicaAJobStore = new RedisJobStore(ioredisClient!); await replicaAJobStore.initialize(); const streamId = `lazy-abort-${Date.now()}`; @@ -675,11 +696,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should abort generation when abort signal received from another replica', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - // This test simulates: // 1. Replica A creates a job and starts generation // 2. Replica B receives abort request and emits abort signal @@ -729,13 +745,8 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should handle wasSyncSent for cross-replica scenarios', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - // Create job directly in Redis with syncSent: true - const jobStore = new RedisJobStore(ioredisClient); + const jobStore = new RedisJobStore(ioredisClient!); await jobStore.initialize(); const streamId = `cross-sync-${Date.now()}`; @@ -762,7 +773,7 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('Sequential Event Ordering (Redis)', () => { + describeRedis('Sequential Event Ordering (Redis)', () => { /** * These tests verify that events are delivered in strict sequential order * when using Redis mode. This is critical because: @@ -773,11 +784,6 @@ describe('GenerationJobManager Integration Tests', () => { * The fix: emitChunk now awaits Redis publish to ensure ordered delivery. */ test('should maintain strict order for rapid sequential emits', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - jest.resetModules(); const services = createStreamServices({ @@ -823,11 +829,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should maintain order for tool call argument deltas', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - jest.resetModules(); const services = createStreamServices({ @@ -882,11 +883,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should maintain order: on_run_step before on_run_step_delta', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - jest.resetModules(); const services = createStreamServices({ @@ -945,11 +941,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should not block other streams when awaiting emitChunk', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - jest.resetModules(); const services = createStreamServices({ @@ -1069,12 +1060,7 @@ describe('GenerationJobManager Integration Tests', () => { await manager.destroy(); }); - test('should buffer and replay events emitted before subscribe (Redis)', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - + testRedis('should buffer and replay events emitted before subscribe (Redis)', async () => { const manager = new GenerationJobManagerClass(); const services = createStreamServices({ useRedis: true, @@ -1118,67 +1104,60 @@ describe('GenerationJobManager Integration Tests', () => { await manager.destroy(); }); - test('should not lose events when emitting before and after subscribe (Redis)', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - - const manager = new GenerationJobManagerClass(); - const services = createStreamServices({ - useRedis: true, - redisClient: ioredisClient, - }); - - manager.configure(services); - manager.initialize(); - - const streamId = `no-loss-${Date.now()}`; - await manager.createJob(streamId, 'user-1'); - - await manager.emitChunk(streamId, { - created: true, - message: { text: 'hello' }, - streamId, - } as unknown as ServerSentEvent); - await manager.emitChunk(streamId, { - event: 'on_run_step', - data: { id: 'step-1', type: 'message_creation', index: 0 }, - }); - - const receivedEvents: unknown[] = []; - const subscription = await manager.subscribe(streamId, (event: unknown) => - receivedEvents.push(event), - ); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - for (let i = 0; i < 10; i++) { - await manager.emitChunk(streamId, { - event: 'on_message_delta', - data: { delta: { content: { type: 'text', text: `word${i} ` } }, index: i }, + testRedis( + 'should not lose events when emitting before and after subscribe (Redis)', + async () => { + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, }); - } - await new Promise((resolve) => setTimeout(resolve, 300)); + manager.configure(services); + manager.initialize(); - expect(receivedEvents.length).toBe(12); - expect((receivedEvents[0] as Record).created).toBe(true); - expect((receivedEvents[1] as Record).event).toBe('on_run_step'); - for (let i = 0; i < 10; i++) { - expect((receivedEvents[i + 2] as Record).event).toBe('on_message_delta'); - } + const streamId = `no-loss-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); - subscription?.unsubscribe(); - await manager.destroy(); - }); + await manager.emitChunk(streamId, { + created: true, + message: { text: 'hello' }, + streamId, + } as unknown as ServerSentEvent); + await manager.emitChunk(streamId, { + event: 'on_run_step', + data: { id: 'step-1', type: 'message_creation', index: 0 }, + }); - test('RedisEventTransport.subscribe() should return a ready promise', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } + const receivedEvents: unknown[] = []; + const subscription = await manager.subscribe(streamId, (event: unknown) => + receivedEvents.push(event), + ); + await new Promise((resolve) => setTimeout(resolve, 100)); + + for (let i = 0; i < 10; i++) { + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `word${i} ` } }, index: i }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + expect(receivedEvents.length).toBe(12); + expect((receivedEvents[0] as Record).created).toBe(true); + expect((receivedEvents[1] as Record).event).toBe('on_run_step'); + for (let i = 0; i < 10; i++) { + expect((receivedEvents[i + 2] as Record).event).toBe('on_message_delta'); + } + + subscription?.unsubscribe(); + await manager.destroy(); + }, + ); + + testRedis('RedisEventTransport.subscribe() should return a ready promise', async () => { const subscriber = (ioredisClient as unknown as { duplicate: () => unknown }).duplicate(); const transport = new RedisEventTransport(ioredisClient as never, subscriber as never); @@ -1211,6 +1190,421 @@ describe('GenerationJobManager Integration Tests', () => { }); }); + describe('Resume: skipBufferReplay prevents duplication', () => { + /** + * Verifies the fix for duplicated content when navigating away from an + * in-progress conversation and back. Events accumulate in earlyEventBuffer + * while the subscriber is absent. On resume, the sync event delivers all + * accumulated content via aggregatedContent, so buffer replay must be + * skipped to prevent duplication. + */ + + test('should NOT replay buffer when skipBufferReplay is true (resume scenario)', async () => { + const manager = createInMemoryManager(); + const streamId = `skip-buf-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + await setupDisconnectedStream(manager, streamId, 10); + + const resumeState = await manager.getResumeState(streamId); + expect(resumeState).not.toBeNull(); + + const resumeEvents: ServerSentEvent[] = []; + const sub2 = await manager.subscribe( + streamId, + (event) => resumeEvents.push(event), + undefined, + undefined, + { skipBufferReplay: true }, + ); + + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(resumeEvents.length).toBe(0); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: ' Live!' } } }, + }); + + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(resumeEvents.length).toBe(1); + expect(resumeEvents[0].event).toBe('on_message_delta'); + + sub2?.unsubscribe(); + await manager.destroy(); + }); + + test('should replay buffer by default when no options are passed', async () => { + const manager = createInMemoryManager(); + const streamId = `replay-buf-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1Events: ServerSentEvent[] = []; + const sub1 = await manager.subscribe(streamId, (event) => sub1Events.push(event)); + await new Promise((resolve) => setTimeout(resolve, 10)); + + await manager.emitChunk(streamId, { + event: 'on_run_step', + data: { id: 'step-1', runId: 'run-1', index: 0, stepDetails: { type: 'message_creation' } }, + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: 'buffered' } } }, + }); + + const sub2Events: ServerSentEvent[] = []; + const sub2 = await manager.subscribe(streamId, (event) => sub2Events.push(event)); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(sub2Events.length).toBe(1); + expect(sub2Events[0].event).toBe('on_message_delta'); + + sub2?.unsubscribe(); + await manager.destroy(); + }); + + test('should clear earlyEventBuffer even when skipping replay (no memory leak)', async () => { + const manager = createInMemoryManager(); + const streamId = `buf-clear-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1 = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 10)); + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'buf1' } } }, + }); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'buf2' } } }, + }); + + const sub2Events: ServerSentEvent[] = []; + const sub2 = await manager.subscribe( + streamId, + (event) => sub2Events.push(event), + undefined, + undefined, + { skipBufferReplay: true }, + ); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(sub2Events.length).toBe(0); + + sub2?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'new-event' } } }, + }); + + const sub3Events: ServerSentEvent[] = []; + const sub3 = await manager.subscribe(streamId, (event) => sub3Events.push(event)); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(sub3Events.length).toBe(1); + const event = sub3Events[0] as { + event: string; + data: { delta: { content: { text: string } } }; + }; + expect(event.data.delta.content.text).toBe('new-event'); + + sub3?.unsubscribe(); + await manager.destroy(); + }); + + test('should handle multiple disconnect/reconnect cycles with skipBufferReplay', async () => { + const manager = createInMemoryManager(); + const streamId = `multi-reconnect-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1 = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 10)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'initial' } } }, + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'buffered-1' } } }, + }); + + const resumeState1 = await manager.getResumeState(streamId); + expect(resumeState1).not.toBeNull(); + + const sub2Events: ServerSentEvent[] = []; + const sub2 = await manager.subscribe( + streamId, + (event) => sub2Events.push(event), + undefined, + undefined, + { skipBufferReplay: true }, + ); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(sub2Events.length).toBe(0); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'live-1' } } }, + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(sub2Events.length).toBe(1); + + sub2?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'buffered-2' } } }, + }); + + const resumeState2 = await manager.getResumeState(streamId); + expect(resumeState2).not.toBeNull(); + + const sub3Events: ServerSentEvent[] = []; + const sub3 = await manager.subscribe( + streamId, + (event) => sub3Events.push(event), + undefined, + undefined, + { skipBufferReplay: true }, + ); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(sub3Events.length).toBe(0); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'live-2' } } }, + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(sub3Events.length).toBe(1); + + sub3?.unsubscribe(); + await manager.destroy(); + }); + + testRedis('should NOT replay buffer when skipBufferReplay is true (Redis)', async () => { + const manager = createRedisManager(); + const streamId = `skip-buf-redis-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + await setupDisconnectedStream(manager, streamId, 100); + + const resumeState = await manager.getResumeState(streamId); + expect(resumeState).not.toBeNull(); + expect(resumeState!.aggregatedContent?.length).toBeGreaterThan(0); + + const resumeEvents: ServerSentEvent[] = []; + const sub2 = await manager.subscribe( + streamId, + (event) => resumeEvents.push(event), + undefined, + undefined, + { skipBufferReplay: true }, + ); + + await new Promise((resolve) => setTimeout(resolve, 200)); + expect(resumeEvents.length).toBe(0); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: ' Live!' } } }, + }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + expect(resumeEvents.length).toBe(1); + expect(resumeEvents[0].event).toBe('on_message_delta'); + + sub2?.unsubscribe(); + await manager.destroy(); + }); + + testRedis( + 'should replay buffer without skipBufferReplay after disconnect (Redis)', + async () => { + const manager = createRedisManager(); + const streamId = `replay-buf-redis-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1 = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 100)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'buffered-redis' } } }, + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const sub2Events: ServerSentEvent[] = []; + const sub2 = await manager.subscribe(streamId, (event) => sub2Events.push(event)); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(sub2Events.length).toBe(1); + expect(sub2Events[0].event).toBe('on_message_delta'); + + sub2?.unsubscribe(); + await manager.destroy(); + }, + ); + }); + + describe('Atomic subscribeWithResume', () => { + test('should return empty pendingEvents for pre-snapshot buffer events (in-memory)', async () => { + const manager = createInMemoryManager(); + const streamId = `atomic-drain-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1 = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 10)); + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + await manager.emitChunk(streamId, { + event: 'on_run_step', + data: { id: 'step-1', runId: 'run-1', index: 0, stepDetails: { type: 'message_creation' } }, + }); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: 'buffered' } } }, + }); + + const liveEvents: ServerSentEvent[] = []; + const { subscription, resumeState, pendingEvents } = await manager.subscribeWithResume( + streamId, + (event) => liveEvents.push(event), + ); + + expect(resumeState).not.toBeNull(); + expect(pendingEvents.length).toBe(0); + expect(liveEvents.length).toBe(0); + + subscription?.unsubscribe(); + await manager.destroy(); + }); + + test('should return empty pendingEvents when buffer is empty', async () => { + const manager = createInMemoryManager(); + const streamId = `atomic-empty-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1 = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 10)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'delivered' } } }, + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + const { pendingEvents } = await manager.subscribeWithResume(streamId, () => {}); + + expect(pendingEvents.length).toBe(0); + + await manager.destroy(); + }); + + test('should deliver live events after subscribeWithResume', async () => { + const manager = createInMemoryManager(); + const streamId = `atomic-live-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1 = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 10)); + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'buffered-pre-snapshot' } } }, + }); + + const liveEvents: ServerSentEvent[] = []; + const { subscription, pendingEvents } = await manager.subscribeWithResume(streamId, (event) => + liveEvents.push(event), + ); + + expect(pendingEvents.length).toBe(0); + expect(liveEvents.length).toBe(0); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'live-after' } } }, + }); + + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(liveEvents.length).toBe(1); + const liveEvent = liveEvents[0] as { + event: string; + data: { delta: { content: { text: string } } }; + }; + expect(liveEvent.data.delta.content.text).toBe('live-after'); + + subscription?.unsubscribe(); + await manager.destroy(); + }); + + testRedis( + 'should return empty pendingEvents in Redis mode (chunks already persisted)', + async () => { + const manager = createRedisManager(); + const streamId = `atomic-redis-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const sub1 = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + sub1?.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 100)); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'buffered-redis' } } }, + }); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const liveEvents: ServerSentEvent[] = []; + const { subscription, resumeState, pendingEvents } = await manager.subscribeWithResume( + streamId, + (event) => liveEvents.push(event), + ); + + expect(resumeState).not.toBeNull(); + expect(pendingEvents.length).toBe(0); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'live-redis' } } }, + }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + expect(liveEvents.length).toBe(1); + + subscription?.unsubscribe(); + await manager.destroy(); + }, + ); + }); + describe('Error Preservation for Late Subscribers', () => { /** * These tests verify the fix for the race condition where errors @@ -1369,14 +1763,9 @@ describe('GenerationJobManager Integration Tests', () => { await GenerationJobManager.destroy(); }); - test('should handle error preservation in Redis mode (cross-replica)', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - + testRedis('should handle error preservation in Redis mode (cross-replica)', async () => { // === Replica A: Creates job and emits error === - const replicaAJobStore = new RedisJobStore(ioredisClient); + const replicaAJobStore = new RedisJobStore(ioredisClient!); await replicaAJobStore.initialize(); const streamId = `redis-error-${Date.now()}`; @@ -1463,13 +1852,8 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('Cross-Replica Live Streaming (Redis)', () => { + describeRedis('Cross-Replica Live Streaming (Redis)', () => { test('should publish events to Redis even when no local subscriber exists', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const replicaA = new GenerationJobManagerClass(); const servicesA = createStreamServices({ useRedis: true, @@ -1489,7 +1873,7 @@ describe('GenerationJobManager Integration Tests', () => { const streamId = `cross-live-${Date.now()}`; await replicaA.createJob(streamId, 'user-1'); - const replicaBJobStore = new RedisJobStore(ioredisClient); + const replicaBJobStore = new RedisJobStore(ioredisClient!); await replicaBJobStore.initialize(); await replicaBJobStore.createJob(streamId, 'user-1'); @@ -1519,11 +1903,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should not cause data loss on cross-replica subscribers when local subscriber joins', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const replicaA = new GenerationJobManagerClass(); const servicesA = createStreamServices({ useRedis: true, @@ -1543,7 +1922,7 @@ describe('GenerationJobManager Integration Tests', () => { const streamId = `cross-seq-safe-${Date.now()}`; await replicaA.createJob(streamId, 'user-1'); - const replicaBJobStore = new RedisJobStore(ioredisClient); + const replicaBJobStore = new RedisJobStore(ioredisClient!); await replicaBJobStore.initialize(); await replicaBJobStore.createJob(streamId, 'user-1'); @@ -1603,11 +1982,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should deliver buffered events locally AND publish live events cross-replica', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const replicaA = new GenerationJobManagerClass(); const servicesA = createStreamServices({ useRedis: true, @@ -1641,7 +2015,7 @@ describe('GenerationJobManager Integration Tests', () => { replicaB.configure(servicesB); replicaB.initialize(); - const replicaBJobStore = new RedisJobStore(ioredisClient); + const replicaBJobStore = new RedisJobStore(ioredisClient!); await replicaBJobStore.initialize(); await replicaBJobStore.createJob(streamId, 'user-1'); @@ -1671,13 +2045,8 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('Concurrent Subscriber Readiness (Redis)', () => { + describeRedis('Concurrent Subscriber Readiness (Redis)', () => { test('should return ready promise to all concurrent subscribers for same stream', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const subscriber = ( ioredisClient as unknown as { duplicate: () => typeof ioredisClient } ).duplicate()!; @@ -1706,13 +2075,8 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('Sequence Reset Safety (Redis)', () => { + describeRedis('Sequence Reset Safety (Redis)', () => { test('should not receive stale pre-subscribe events via Redis after sequence reset', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const manager = new GenerationJobManagerClass(); const services = createStreamServices({ useRedis: true, @@ -1774,11 +2138,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should not reset sequence when second subscriber joins mid-stream', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const manager = new GenerationJobManagerClass(); const services = createStreamServices({ useRedis: true, @@ -1837,13 +2196,8 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('Subscribe Error Recovery (Redis)', () => { + describeRedis('Subscribe Error Recovery (Redis)', () => { test('should allow resubscription after Redis subscribe failure', async () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - const subscriber = ( ioredisClient as unknown as { duplicate: () => typeof ioredisClient } ).duplicate()!; @@ -1892,12 +2246,7 @@ describe('GenerationJobManager Integration Tests', () => { }); describe('createStreamServices Auto-Detection', () => { - test('should use Redis when useRedis is true and client is available', () => { - if (!ioredisClient) { - console.warn('Redis not available, skipping test'); - return; - } - + testRedis('should use Redis when useRedis is true and client is available', () => { const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, diff --git a/packages/api/src/types/stream.ts b/packages/api/src/types/stream.ts index 79b29d774f..068d9c8db8 100644 --- a/packages/api/src/types/stream.ts +++ b/packages/api/src/types/stream.ts @@ -47,3 +47,24 @@ export type ChunkHandler = (event: ServerSentEvent) => void; export type DoneHandler = (event: ServerSentEvent) => void; export type ErrorHandler = (error: string) => void; export type UnsubscribeFn = () => void; + +/** Options for subscribing to a job event stream */ +export interface SubscribeOptions { + /** + * When true, skips replaying the earlyEventBuffer. + * Use for resume connections after a sync event has been sent. + */ + skipBufferReplay?: boolean; +} + +/** Result of an atomic subscribe-with-resume operation */ +export interface SubscribeWithResumeResult { + subscription: { unsubscribe: UnsubscribeFn } | null; + resumeState: ResumeState | null; + /** + * Events that arrived between the resume snapshot and the subscribe call. + * In-memory mode: drained from earlyEventBuffer (only place they exist). + * Redis mode: empty — chunks are persisted to the store and appear in aggregatedContent on next resume. + */ + pendingEvents: ServerSentEvent[]; +} From 83184467043016d3e48b0d86c212d73b510eefa3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 21:22:25 -0400 Subject: [PATCH 033/111] =?UTF-8?q?=F0=9F=92=81=20refactor:=20Better=20Con?= =?UTF-8?q?fig=20UX=20for=20MCP=20STDIO=20with=20`customUserVars`=20(#1222?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Better UX for MCP stdio with Custom User Variables - Updated the ConnectionsRepository to prevent connections when customUserVars are defined, improving security and access control. - Modified the MCPServerInspector to skip capabilities fetch when customUserVars are present, streamlining server inspection. - Added tests to validate connection restrictions with customUserVars, ensuring robust handling of various server configurations. This change enhances the overall integrity of the connection management process by enforcing stricter rules around custom user variables. * fix: guard against empty customUserVars and add JSDoc context - Extract `hasCustomUserVars()` helper to guard against truthy `{}` (Zod's `.record().optional()` yields `{}` on empty input, not `undefined`) - Add JSDoc to `isAllowedToConnectToServer` explaining why customUserVars servers are excluded from app-level connections * test: improve customUserVars test coverage and fixture hygiene - Add no-connection-provided test for MCPServerInspector (production path) - Fix test descriptions to match actual fixture values - Replace real package name with fictional @test/mcp-stdio-server --- packages/api/src/mcp/ConnectionsRepository.ts | 18 +++++-- .../__tests__/ConnectionsRepository.test.ts | 44 +++++++++++++++++ .../src/mcp/registry/MCPServerInspector.ts | 7 ++- .../__tests__/MCPServerInspector.test.ts | 48 +++++++++++++++++++ packages/api/src/mcp/utils.ts | 5 ++ 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/packages/api/src/mcp/ConnectionsRepository.ts b/packages/api/src/mcp/ConnectionsRepository.ts index e629934dda..970e7ea4b9 100644 --- a/packages/api/src/mcp/ConnectionsRepository.ts +++ b/packages/api/src/mcp/ConnectionsRepository.ts @@ -1,8 +1,9 @@ import { logger } from '@librechat/data-schemas'; -import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; -import { MCPConnection } from './connection'; -import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; import type * as t from './types'; +import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; +import { hasCustomUserVars } from './utils'; +import { MCPConnection } from './connection'; const CONNECT_CONCURRENCY = 3; @@ -139,12 +140,19 @@ export class ConnectionsRepository { return `[MCP][${serverName}]`; } + /** + * App-level (shared) connections cannot serve servers that need per-user context: + * env/header placeholders like `{{MY_KEY}}` are only resolved by `processMCPEnv()` + * when real `customUserVars` values exist — which requires a user-level connection. + */ private isAllowedToConnectToServer(config: t.ParsedServerConfig) { if (config.inspectionFailed) { return false; } - //the repository is not allowed to be connected in case the Connection repository is shared (ownerId is undefined/null) and the server requires Auth or startup false. - if (this.ownerId === undefined && (config.startup === false || config.requiresOAuth)) { + if ( + this.ownerId === undefined && + (config.startup === false || config.requiresOAuth || hasCustomUserVars(config)) + ) { return false; } return true; diff --git a/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts b/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts index 3b827774d0..98e15eca18 100644 --- a/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts +++ b/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts @@ -392,6 +392,36 @@ describe('ConnectionsRepository', () => { expect(await repository.has('oauthDisabledServer')).toBe(false); }); + it('should NOT allow connection to servers with customUserVars', async () => { + mockServerConfigs.customVarServer = { + type: 'stdio', + command: 'npx', + args: ['-y', '@test/mcp-stdio-server'], + env: { API_KEY: '{{MY_KEY}}' }, + customUserVars: { + MY_KEY: { title: 'API Key', description: 'Your API key' }, + }, + }; + + expect(await repository.has('customVarServer')).toBe(false); + }); + + it('should NOT allow connection when customUserVars is defined, even when startup is explicitly true', async () => { + mockServerConfigs.customVarStartupServer = { + type: 'stdio', + command: 'npx', + args: ['-y', '@test/mcp-stdio-server'], + env: { TOKEN: '{{USER_TOKEN}}' }, + startup: true, + requiresOAuth: false, + customUserVars: { + USER_TOKEN: { title: 'Token', description: 'Your token' }, + }, + }; + + expect(await repository.has('customVarStartupServer')).toBe(false); + }); + it('should disconnect existing connection when server becomes not allowed', async () => { // Initially setup as regular server mockServerConfigs.changingServer = { @@ -471,6 +501,20 @@ describe('ConnectionsRepository', () => { expect(await repository.has('oauthDisabledServer')).toBe(true); }); + it('should allow connection to servers with customUserVars', async () => { + mockServerConfigs.customVarServer = { + type: 'stdio', + command: 'npx', + args: ['-y', '@test/mcp-stdio-server'], + env: { API_KEY: '{{MY_KEY}}' }, + customUserVars: { + MY_KEY: { title: 'API Key', description: 'Your API key' }, + }, + }; + + expect(await repository.has('customVarServer')).toBe(true); + }); + it('should return null from get() when server config does not exist', async () => { const connection = await repository.get('nonexistent'); expect(connection).toBeNull(); diff --git a/packages/api/src/mcp/registry/MCPServerInspector.ts b/packages/api/src/mcp/registry/MCPServerInspector.ts index eea52bbf2e..a477d9b412 100644 --- a/packages/api/src/mcp/registry/MCPServerInspector.ts +++ b/packages/api/src/mcp/registry/MCPServerInspector.ts @@ -6,6 +6,7 @@ import { isMCPDomainAllowed, extractMCPServerDomain } from '~/auth/domain'; import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; import { MCPDomainNotAllowedError } from '~/mcp/errors'; import { detectOAuthRequirement } from '~/mcp/oauth'; +import { hasCustomUserVars } from '~/mcp/utils'; import { isEnabled } from '~/utils'; /** @@ -54,7 +55,11 @@ export class MCPServerInspector { private async inspectServer(): Promise { await this.detectOAuth(); - if (this.config.startup !== false && !this.config.requiresOAuth) { + if ( + this.config.startup !== false && + !this.config.requiresOAuth && + !hasCustomUserVars(this.config) + ) { let tempConnection = false; if (!this.connection) { tempConnection = true; diff --git a/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts b/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts index b79f2d044a..f0ab75c9b4 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts @@ -100,6 +100,54 @@ describe('MCPServerInspector', () => { }); }); + it('should skip capabilities fetch when customUserVars is defined', async () => { + const rawConfig: t.MCPOptions = { + type: 'stdio', + command: 'npx', + args: ['-y', '@test/mcp-stdio-server'], + env: { API_KEY: '{{MY_KEY}}' }, + customUserVars: { + MY_KEY: { title: 'API Key', description: 'Your API key' }, + }, + }; + + const result = await MCPServerInspector.inspect('test_server', rawConfig, mockConnection); + + expect(result).toEqual({ + type: 'stdio', + command: 'npx', + args: ['-y', '@test/mcp-stdio-server'], + env: { API_KEY: '{{MY_KEY}}' }, + customUserVars: { + MY_KEY: { title: 'API Key', description: 'Your API key' }, + }, + requiresOAuth: false, + initDuration: expect.any(Number), + }); + + expect(MCPConnectionFactory.create).not.toHaveBeenCalled(); + expect(mockConnection.disconnect).not.toHaveBeenCalled(); + }); + + it('should NOT create a temp connection when customUserVars is defined and no connection is provided', async () => { + const rawConfig: t.MCPOptions = { + type: 'stdio', + command: 'npx', + args: ['-y', '@test/mcp-stdio-server'], + env: { API_KEY: '{{MY_KEY}}' }, + customUserVars: { + MY_KEY: { title: 'API Key', description: 'Your API key' }, + }, + }; + + const result = await MCPServerInspector.inspect('test_server', rawConfig); + + expect(MCPConnectionFactory.create).not.toHaveBeenCalled(); + expect(result.requiresOAuth).toBe(false); + expect(result.capabilities).toBeUndefined(); + expect(result.toolFunctions).toBeUndefined(); + }); + it('should keep custom serverInstructions string and not fetch from server', async () => { const rawConfig: t.MCPOptions = { type: 'stdio', diff --git a/packages/api/src/mcp/utils.ts b/packages/api/src/mcp/utils.ts index c517388a76..ff367725fc 100644 --- a/packages/api/src/mcp/utils.ts +++ b/packages/api/src/mcp/utils.ts @@ -3,6 +3,11 @@ import type { ParsedServerConfig } from '~/mcp/types'; export const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`); +/** Checks that `customUserVars` is present AND non-empty (guards against truthy `{}`) */ +export function hasCustomUserVars(config: Pick): boolean { + return !!config.customUserVars && Object.keys(config.customUserVars).length > 0; +} + /** * Allowlist-based sanitization for API responses. Only explicitly listed fields are included; * new fields added to ParsedServerConfig are excluded by default until allowlisted here. From 7c39a4594463442d07d09bba03c4e24639d59d70 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Mar 2026 22:43:18 -0400 Subject: [PATCH 034/111] =?UTF-8?q?=F0=9F=90=8D=20refactor:=20Normalize=20?= =?UTF-8?q?Non-Standard=20Browser=20MIME=20Type=20Aliases=20in=20`inferMim?= =?UTF-8?q?eType`=20(#12240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: Normalize non-standard browser MIME types in inferMimeType macOS Chrome/Firefox report .py files as text/x-python-script instead of text/x-python, causing client-side validation to reject Python file uploads. inferMimeType now normalizes known MIME type aliases before returning, so non-standard variants match the accepted regex patterns. * 🧪 test: Add tests for MIME type alias normalization in inferMimeType * 🐛 fix: Restore JSDoc params and make mimeTypeAliases immutable * 🧪 test: Add checkType integration tests, remove redundant DragDropModal tests --- .../data-provider/src/file-config.spec.ts | 41 ++++++++++++++++++- packages/data-provider/src/file-config.ts | 16 +++++--- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/data-provider/src/file-config.spec.ts b/packages/data-provider/src/file-config.spec.ts index 018b4dbfcf..0ab9f23a3e 100644 --- a/packages/data-provider/src/file-config.spec.ts +++ b/packages/data-provider/src/file-config.spec.ts @@ -1,15 +1,52 @@ import type { FileConfig } from './types/files'; import { fileConfig as baseFileConfig, + documentParserMimeTypes, getEndpointFileConfig, - mergeFileConfig, applicationMimeTypes, defaultOCRMimeTypes, - documentParserMimeTypes, supportedMimeTypes, + mergeFileConfig, + inferMimeType, + textMimeTypes, } from './file-config'; import { EModelEndpoint } from './schemas'; +describe('inferMimeType', () => { + it('should normalize text/x-python-script to text/x-python', () => { + expect(inferMimeType('test.py', 'text/x-python-script')).toBe('text/x-python'); + }); + + it('should return a type that matches textMimeTypes after normalization', () => { + const normalized = inferMimeType('test.py', 'text/x-python-script'); + expect(textMimeTypes.test(normalized)).toBe(true); + }); + + it('should pass through standard browser types unchanged', () => { + expect(inferMimeType('test.py', 'text/x-python')).toBe('text/x-python'); + expect(inferMimeType('doc.pdf', 'application/pdf')).toBe('application/pdf'); + }); + + it('should infer from extension when browser type is empty', () => { + expect(inferMimeType('test.py', '')).toBe('text/x-python'); + expect(inferMimeType('code.js', '')).toBe('text/javascript'); + expect(inferMimeType('photo.heic', '')).toBe('image/heic'); + }); + + it('should return empty string for unknown extension with no browser type', () => { + expect(inferMimeType('file.xyz', '')).toBe(''); + }); + + it('should produce a type accepted by checkType after normalizing text/x-python-script', () => { + const normalized = inferMimeType('test.py', 'text/x-python-script'); + expect(baseFileConfig.checkType(normalized)).toBe(true); + }); + + it('should reject raw text/x-python-script without normalization', () => { + expect(baseFileConfig.checkType('text/x-python-script')).toBe(false); + }); +}); + describe('applicationMimeTypes', () => { const odfTypes = [ 'application/vnd.oasis.opendocument.text', diff --git a/packages/data-provider/src/file-config.ts b/packages/data-provider/src/file-config.ts index 033c868a80..67b4197958 100644 --- a/packages/data-provider/src/file-config.ts +++ b/packages/data-provider/src/file-config.ts @@ -357,15 +357,21 @@ export const imageTypeMapping: { [key: string]: string } = { heif: 'image/heif', }; +/** Normalizes non-standard MIME types that browsers may report to their canonical forms */ +export const mimeTypeAliases: Readonly> = { + 'text/x-python-script': 'text/x-python', +}; + /** - * Infers the MIME type from a file's extension when the browser doesn't recognize it - * @param fileName - The name of the file including extension - * @param currentType - The current MIME type reported by the browser (may be empty) - * @returns The inferred MIME type if browser didn't provide one, otherwise the original type + * Infers the MIME type from a file's extension when the browser doesn't recognize it, + * and normalizes known non-standard MIME type aliases to their canonical forms. + * @param fileName - The file name including its extension + * @param currentType - The MIME type reported by the browser (may be empty string) + * @returns The normalized or inferred MIME type; empty string if unresolvable */ export function inferMimeType(fileName: string, currentType: string): string { if (currentType) { - return currentType; + return mimeTypeAliases[currentType] ?? currentType; } const extension = fileName.split('.').pop()?.toLowerCase() ?? ''; From 0c27ad2d55e7229cc43f4c72111fa8833369f60f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 10:19:29 -0400 Subject: [PATCH 035/111] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20refactor:=20Sco?= =?UTF-8?q?pe=20Action=20Mutations=20by=20Parent=20Resource=20Ownership=20?= =?UTF-8?q?(#12237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Scope action mutations by parent resource ownership Prevent cross-tenant action overwrites by validating that an existing action's agent_id/assistant_id matches the URL parameter before allowing updates or deletes. Without this, a user with EDIT access on their own agent could reference a foreign action_id to hijack another agent's action record. * 🛡️ fix: Harden action ownership checks and scope write filters - Remove && short-circuit that bypassed the guard when agent_id or assistant_id was falsy (e.g. assistant-owned actions have no agent_id, so the check was skipped entirely on the agents route). - Include agent_id / assistant_id in the updateAction and deleteAction query filters so the DB write itself enforces ownership atomically. - Log a warning when deleteAction returns null (silent no-op from data-integrity mismatch). * 📝 docs: Update Action model JSDoc to reflect scoped query params * ✅ test: Add Action ownership scoping tests Cover update, delete, and cross-type protection scenarios using MongoMemoryServer to verify that scoped query filters (agent_id, assistant_id) prevent cross-tenant overwrites and deletions at the database level. * 🛡️ fix: Scope updateAction filter in agent duplication handler * 🐛 fix: Use action metadata domain instead of action_id when duplicating agent actions The duplicate handler was splitting `action.action_id` by `actionDelimiter` to extract the domain, but `action_id` is a bare nanoid that doesn't contain the delimiter. This produced malformed entries in the duplicated agent's actions array (nanoid_action_newNanoid instead of domain_action_newNanoid). The domain is available on `action.metadata.domain`. * ✅ test: Add integration tests for agent duplication action handling Uses MongoMemoryServer with real Agent and Action models to verify: - Duplicated actions use metadata.domain (not action_id) for the agent actions array entries - Sensitive metadata fields are stripped from duplicated actions - Original action documents are not modified --- api/models/Action.js | 10 +- api/models/Action.spec.js | 250 ++++++++++++++++++ .../__tests__/v1.duplicate-actions.spec.js | 159 +++++++++++ api/server/controllers/agents/v1.js | 4 +- api/server/routes/agents/actions.js | 13 +- api/server/routes/assistants/actions.js | 15 +- 6 files changed, 437 insertions(+), 14 deletions(-) create mode 100644 api/models/Action.spec.js create mode 100644 api/server/controllers/agents/__tests__/v1.duplicate-actions.spec.js diff --git a/api/models/Action.js b/api/models/Action.js index 20aa20a7e4..f14c415d5b 100644 --- a/api/models/Action.js +++ b/api/models/Action.js @@ -4,9 +4,7 @@ const { Action } = require('~/db/models'); * Update an action with new data without overwriting existing properties, * or create a new action if it doesn't exist. * - * @param {Object} searchParams - The search parameters to find the action to update. - * @param {string} searchParams.action_id - The ID of the action to update. - * @param {string} searchParams.user - The user ID of the action's author. + * @param {{ action_id: string, agent_id?: string, assistant_id?: string, user?: string }} searchParams * @param {Object} updateData - An object containing the properties to update. * @returns {Promise} The updated or newly created action document as a plain object. */ @@ -47,10 +45,8 @@ const getActions = async (searchParams, includeSensitive = false) => { /** * Deletes an action by params. * - * @param {Object} searchParams - The search parameters to find the action to delete. - * @param {string} searchParams.action_id - The ID of the action to delete. - * @param {string} searchParams.user - The user ID of the action's author. - * @returns {Promise} A promise that resolves to the deleted action document as a plain object, or null if no document was found. + * @param {{ action_id: string, agent_id?: string, assistant_id?: string, user?: string }} searchParams + * @returns {Promise} The deleted action document as a plain object, or null if no match. */ const deleteAction = async (searchParams) => { return await Action.findOneAndDelete(searchParams).lean(); diff --git a/api/models/Action.spec.js b/api/models/Action.spec.js new file mode 100644 index 0000000000..61a3b10f0f --- /dev/null +++ b/api/models/Action.spec.js @@ -0,0 +1,250 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { actionSchema } = require('@librechat/data-schemas'); +const { updateAction, getActions, deleteAction } = require('./Action'); + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + if (!mongoose.models.Action) { + mongoose.model('Action', actionSchema); + } + await mongoose.connect(mongoUri); +}, 20000); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await mongoose.models.Action.deleteMany({}); +}); + +const userId = new mongoose.Types.ObjectId(); + +describe('Action ownership scoping', () => { + describe('updateAction', () => { + it('updates when action_id and agent_id both match', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_1', + agent_id: 'agent_A', + metadata: { domain: 'example.com' }, + }); + + const result = await updateAction( + { action_id: 'act_1', agent_id: 'agent_A' }, + { metadata: { domain: 'updated.com' } }, + ); + + expect(result).not.toBeNull(); + expect(result.metadata.domain).toBe('updated.com'); + expect(result.agent_id).toBe('agent_A'); + }); + + it('does not update when agent_id does not match (creates a new doc via upsert)', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_1', + agent_id: 'agent_B', + metadata: { domain: 'victim.com', api_key: 'secret' }, + }); + + const result = await updateAction( + { action_id: 'act_1', agent_id: 'agent_A' }, + { user: userId, metadata: { domain: 'attacker.com' } }, + ); + + expect(result.metadata.domain).toBe('attacker.com'); + + const original = await mongoose.models.Action.findOne({ + action_id: 'act_1', + agent_id: 'agent_B', + }).lean(); + expect(original).not.toBeNull(); + expect(original.metadata.domain).toBe('victim.com'); + expect(original.metadata.api_key).toBe('secret'); + }); + + it('updates when action_id and assistant_id both match', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_2', + assistant_id: 'asst_X', + metadata: { domain: 'example.com' }, + }); + + const result = await updateAction( + { action_id: 'act_2', assistant_id: 'asst_X' }, + { metadata: { domain: 'updated.com' } }, + ); + + expect(result).not.toBeNull(); + expect(result.metadata.domain).toBe('updated.com'); + }); + + it('does not overwrite when assistant_id does not match', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_2', + assistant_id: 'asst_victim', + metadata: { domain: 'victim.com', api_key: 'secret' }, + }); + + await updateAction( + { action_id: 'act_2', assistant_id: 'asst_attacker' }, + { user: userId, metadata: { domain: 'attacker.com' } }, + ); + + const original = await mongoose.models.Action.findOne({ + action_id: 'act_2', + assistant_id: 'asst_victim', + }).lean(); + expect(original).not.toBeNull(); + expect(original.metadata.domain).toBe('victim.com'); + expect(original.metadata.api_key).toBe('secret'); + }); + }); + + describe('deleteAction', () => { + it('deletes when action_id and agent_id both match', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_del', + agent_id: 'agent_A', + metadata: { domain: 'example.com' }, + }); + + const result = await deleteAction({ action_id: 'act_del', agent_id: 'agent_A' }); + expect(result).not.toBeNull(); + expect(result.action_id).toBe('act_del'); + + const remaining = await mongoose.models.Action.countDocuments(); + expect(remaining).toBe(0); + }); + + it('returns null and preserves the document when agent_id does not match', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_del', + agent_id: 'agent_B', + metadata: { domain: 'victim.com' }, + }); + + const result = await deleteAction({ action_id: 'act_del', agent_id: 'agent_A' }); + expect(result).toBeNull(); + + const remaining = await mongoose.models.Action.countDocuments(); + expect(remaining).toBe(1); + }); + + it('deletes when action_id and assistant_id both match', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_del_asst', + assistant_id: 'asst_X', + metadata: { domain: 'example.com' }, + }); + + const result = await deleteAction({ action_id: 'act_del_asst', assistant_id: 'asst_X' }); + expect(result).not.toBeNull(); + + const remaining = await mongoose.models.Action.countDocuments(); + expect(remaining).toBe(0); + }); + + it('returns null and preserves the document when assistant_id does not match', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_del_asst', + assistant_id: 'asst_victim', + metadata: { domain: 'victim.com' }, + }); + + const result = await deleteAction({ + action_id: 'act_del_asst', + assistant_id: 'asst_attacker', + }); + expect(result).toBeNull(); + + const remaining = await mongoose.models.Action.countDocuments(); + expect(remaining).toBe(1); + }); + }); + + describe('getActions (unscoped baseline)', () => { + it('returns actions by action_id regardless of agent_id', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_shared', + agent_id: 'agent_B', + metadata: { domain: 'example.com' }, + }); + + const results = await getActions({ action_id: 'act_shared' }, true); + expect(results).toHaveLength(1); + expect(results[0].agent_id).toBe('agent_B'); + }); + + it('returns actions scoped by agent_id when provided', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_scoped', + agent_id: 'agent_A', + metadata: { domain: 'a.com' }, + }); + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_other', + agent_id: 'agent_B', + metadata: { domain: 'b.com' }, + }); + + const results = await getActions({ agent_id: 'agent_A' }); + expect(results).toHaveLength(1); + expect(results[0].action_id).toBe('act_scoped'); + }); + }); + + describe('cross-type protection', () => { + it('updateAction with agent_id filter does not overwrite assistant-owned action', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_cross', + assistant_id: 'asst_victim', + metadata: { domain: 'victim.com', api_key: 'secret' }, + }); + + await updateAction( + { action_id: 'act_cross', agent_id: 'agent_attacker' }, + { user: userId, metadata: { domain: 'evil.com' } }, + ); + + const original = await mongoose.models.Action.findOne({ + action_id: 'act_cross', + assistant_id: 'asst_victim', + }).lean(); + expect(original).not.toBeNull(); + expect(original.metadata.domain).toBe('victim.com'); + expect(original.metadata.api_key).toBe('secret'); + }); + + it('deleteAction with agent_id filter does not delete assistant-owned action', async () => { + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_cross_del', + assistant_id: 'asst_victim', + metadata: { domain: 'victim.com' }, + }); + + const result = await deleteAction({ action_id: 'act_cross_del', agent_id: 'agent_attacker' }); + expect(result).toBeNull(); + + const remaining = await mongoose.models.Action.countDocuments(); + expect(remaining).toBe(1); + }); + }); +}); diff --git a/api/server/controllers/agents/__tests__/v1.duplicate-actions.spec.js b/api/server/controllers/agents/__tests__/v1.duplicate-actions.spec.js new file mode 100644 index 0000000000..cc298bd03a --- /dev/null +++ b/api/server/controllers/agents/__tests__/v1.duplicate-actions.spec.js @@ -0,0 +1,159 @@ +jest.mock('~/server/services/PermissionService', () => ({ + findPubliclyAccessibleResources: jest.fn(), + findAccessibleResources: jest.fn(), + hasPublicPermission: jest.fn(), + grantPermission: jest.fn().mockResolvedValue({}), +})); + +jest.mock('~/server/services/Config', () => ({ + getCachedTools: jest.fn(), + getMCPServerTools: jest.fn(), +})); + +const mongoose = require('mongoose'); +const { actionDelimiter } = require('librechat-data-provider'); +const { agentSchema, actionSchema } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { duplicateAgent } = require('../v1'); + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + if (!mongoose.models.Agent) { + mongoose.model('Agent', agentSchema); + } + if (!mongoose.models.Action) { + mongoose.model('Action', actionSchema); + } + await mongoose.connect(mongoUri); +}, 20000); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await mongoose.models.Agent.deleteMany({}); + await mongoose.models.Action.deleteMany({}); +}); + +describe('duplicateAgentHandler — action domain extraction', () => { + it('builds duplicated action entries using metadata.domain, not action_id', async () => { + const userId = new mongoose.Types.ObjectId(); + const originalAgentId = `agent_original`; + + const agent = await mongoose.models.Agent.create({ + id: originalAgentId, + name: 'Test Agent', + author: userId.toString(), + provider: 'openai', + model: 'gpt-4', + tools: [], + actions: [`api.example.com${actionDelimiter}act_original`], + versions: [{ name: 'Test Agent', createdAt: new Date(), updatedAt: new Date() }], + }); + + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_original', + agent_id: originalAgentId, + metadata: { domain: 'api.example.com' }, + }); + + const req = { + params: { id: agent.id }, + user: { id: userId.toString() }, + }; + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + await duplicateAgent(req, res); + + expect(res.status).toHaveBeenCalledWith(201); + + const { agent: newAgent, actions: newActions } = res.json.mock.calls[0][0]; + + expect(newAgent.id).not.toBe(originalAgentId); + expect(String(newAgent.author)).toBe(userId.toString()); + expect(newActions).toHaveLength(1); + expect(newActions[0].metadata.domain).toBe('api.example.com'); + expect(newActions[0].agent_id).toBe(newAgent.id); + + for (const actionEntry of newAgent.actions) { + const [domain, actionId] = actionEntry.split(actionDelimiter); + expect(domain).toBe('api.example.com'); + expect(actionId).toBeTruthy(); + expect(actionId).not.toBe('act_original'); + } + + const allActions = await mongoose.models.Action.find({}).lean(); + expect(allActions).toHaveLength(2); + + const originalAction = allActions.find((a) => a.action_id === 'act_original'); + expect(originalAction.agent_id).toBe(originalAgentId); + + const duplicatedAction = allActions.find((a) => a.action_id !== 'act_original'); + expect(duplicatedAction.agent_id).toBe(newAgent.id); + expect(duplicatedAction.metadata.domain).toBe('api.example.com'); + }); + + it('strips sensitive metadata fields from duplicated actions', async () => { + const userId = new mongoose.Types.ObjectId(); + const originalAgentId = 'agent_sensitive'; + + await mongoose.models.Agent.create({ + id: originalAgentId, + name: 'Sensitive Agent', + author: userId.toString(), + provider: 'openai', + model: 'gpt-4', + tools: [], + actions: [`secure.api.com${actionDelimiter}act_secret`], + versions: [{ name: 'Sensitive Agent', createdAt: new Date(), updatedAt: new Date() }], + }); + + await mongoose.models.Action.create({ + user: userId, + action_id: 'act_secret', + agent_id: originalAgentId, + metadata: { + domain: 'secure.api.com', + api_key: 'sk-secret-key-12345', + oauth_client_id: 'client_id_xyz', + oauth_client_secret: 'client_secret_xyz', + }, + }); + + const req = { + params: { id: originalAgentId }, + user: { id: userId.toString() }, + }; + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + await duplicateAgent(req, res); + + expect(res.status).toHaveBeenCalledWith(201); + + const duplicatedAction = await mongoose.models.Action.findOne({ + agent_id: { $ne: originalAgentId }, + }).lean(); + + expect(duplicatedAction.metadata.domain).toBe('secure.api.com'); + expect(duplicatedAction.metadata.api_key).toBeUndefined(); + expect(duplicatedAction.metadata.oauth_client_id).toBeUndefined(); + expect(duplicatedAction.metadata.oauth_client_secret).toBeUndefined(); + + const originalAction = await mongoose.models.Action.findOne({ + action_id: 'act_secret', + }).lean(); + expect(originalAction.metadata.api_key).toBe('sk-secret-key-12345'); + }); +}); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index a2c0d55186..1abba8b2c8 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -371,7 +371,7 @@ const duplicateAgentHandler = async (req, res) => { */ const duplicateAction = async (action) => { const newActionId = nanoid(); - const [domain] = action.action_id.split(actionDelimiter); + const { domain } = action.metadata; const fullActionId = `${domain}${actionDelimiter}${newActionId}`; // Sanitize sensitive metadata before persisting @@ -381,7 +381,7 @@ const duplicateAgentHandler = async (req, res) => { } const newAction = await updateAction( - { action_id: newActionId }, + { action_id: newActionId, agent_id: newAgentId }, { metadata: filteredMetadata, agent_id: newAgentId, diff --git a/api/server/routes/agents/actions.js b/api/server/routes/agents/actions.js index 12168ba28a..4643f096aa 100644 --- a/api/server/routes/agents/actions.js +++ b/api/server/routes/agents/actions.js @@ -143,6 +143,9 @@ router.post( if (actions_result && actions_result.length) { const action = actions_result[0]; + if (action.agent_id !== agent_id) { + return res.status(403).json({ message: 'Action does not belong to this agent' }); + } metadata = { ...action.metadata, ...metadata }; } @@ -184,7 +187,7 @@ router.post( } /** @type {[Action]} */ - const updatedAction = await updateAction({ action_id }, actionUpdateData); + const updatedAction = await updateAction({ action_id, agent_id }, actionUpdateData); const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret']; for (let field of sensitiveFields) { @@ -251,7 +254,13 @@ router.delete( { tools: updatedTools, actions: updatedActions }, { updatingUserId: req.user.id, forceVersion: true }, ); - await deleteAction({ action_id }); + const deleted = await deleteAction({ action_id, agent_id }); + if (!deleted) { + logger.warn('[Agent Action Delete] No matching action document found', { + action_id, + agent_id, + }); + } res.status(200).json({ message: 'Action deleted successfully' }); } catch (error) { const message = 'Trouble deleting the Agent Action'; diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js index 57975d32a7..b085fbd36a 100644 --- a/api/server/routes/assistants/actions.js +++ b/api/server/routes/assistants/actions.js @@ -60,6 +60,9 @@ router.post('/:assistant_id', async (req, res) => { if (actions_result && actions_result.length) { const action = actions_result[0]; + if (action.assistant_id !== assistant_id) { + return res.status(403).json({ message: 'Action does not belong to this assistant' }); + } metadata = { ...action.metadata, ...metadata }; } @@ -117,7 +120,7 @@ router.post('/:assistant_id', async (req, res) => { // For new actions, use the assistant owner's user ID actionUpdateData.user = assistant_user || req.user.id; } - promises.push(updateAction({ action_id }, actionUpdateData)); + promises.push(updateAction({ action_id, assistant_id }, actionUpdateData)); /** @type {[AssistantDocument, Action]} */ let [assistantDocument, updatedAction] = await Promise.all(promises); @@ -196,9 +199,15 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => { assistantUpdateData.user = req.user.id; } promises.push(updateAssistantDoc({ assistant_id }, assistantUpdateData)); - promises.push(deleteAction({ action_id })); + promises.push(deleteAction({ action_id, assistant_id })); - await Promise.all(promises); + const [, deletedAction] = await Promise.all(promises); + if (!deletedAction) { + logger.warn('[Assistant Action Delete] No matching action document found', { + action_id, + assistant_id, + }); + } res.status(200).json({ message: 'Action deleted successfully' }); } catch (error) { const message = 'Trouble deleting the Assistant Action'; From 93a628d7a2c2ba07b1c92a118a7d0774da9d706e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 10:35:44 -0400 Subject: [PATCH 036/111] =?UTF-8?q?=F0=9F=93=8E=20fix:=20Respect=20fileCon?= =?UTF-8?q?fig.disabled=20for=20Agents=20Endpoint=20Upload=20Button=20(#12?= =?UTF-8?q?238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: respect fileConfig.disabled for agents endpoint upload button The isAgents check was OR'd without the !isUploadDisabled guard, bypassing the fileConfig.endpoints.agents.disabled setting and always rendering the attach file menu for agents. * test: add regression tests for fileConfig.disabled upload guard Cover the isUploadDisabled rendering gate for agents and assistants endpoints, preventing silent reintroduction of the bypass bug. * test: cover disabled fallback chain in useAgentFileConfig Verify agents-disabled propagates when no provider is set, when provider has no specific config (agents as fallback), and that provider-specific enabled overrides agents disabled. --- .../Chat/Input/Files/AttachFileChat.tsx | 2 +- .../Files/__tests__/AttachFileChat.spec.tsx | 59 ++++++++++++++++++- .../Agents/__tests__/AgentFileConfig.spec.tsx | 47 ++++++++++++++- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/client/src/components/Chat/Input/Files/AttachFileChat.tsx b/client/src/components/Chat/Input/Files/AttachFileChat.tsx index 00a0b7aaa8..2f954d01d5 100644 --- a/client/src/components/Chat/Input/Files/AttachFileChat.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileChat.tsx @@ -91,7 +91,7 @@ function AttachFileChat({ if (isAssistants && endpointSupportsFiles && !isUploadDisabled) { return ; - } else if (isAgents || (endpointSupportsFiles && !isUploadDisabled)) { + } else if ((isAgents || endpointSupportsFiles) && !isUploadDisabled) { return ( > = {}; let mockAgentQueryData: Partial | undefined; @@ -65,6 +67,7 @@ function renderComponent(conversation: Record | null, disableIn describe('AttachFileChat', () => { beforeEach(() => { + mockFileConfig = defaultFileConfig; mockAgentsMap = {}; mockAgentQueryData = undefined; mockAttachFileMenuProps = {}; @@ -148,6 +151,60 @@ describe('AttachFileChat', () => { }); }); + describe('upload disabled rendering', () => { + it('renders null for agents endpoint when fileConfig.agents.disabled is true', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { disabled: true }, + }, + }); + const { container } = renderComponent({ + endpoint: EModelEndpoint.agents, + agent_id: 'agent-1', + }); + expect(container.innerHTML).toBe(''); + }); + + it('renders null for agents endpoint when disableInputs is true', () => { + const { container } = renderComponent( + { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }, + true, + ); + expect(container.innerHTML).toBe(''); + }); + + it('renders AttachFile for assistants endpoint when not disabled', () => { + renderComponent({ endpoint: EModelEndpoint.assistants }); + expect(screen.getByTestId('attach-file')).toBeInTheDocument(); + }); + + it('renders AttachFileMenu when provider-specific config overrides agents disabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { disabled: false, fileLimit: 5 }, + [EModelEndpoint.agents]: { disabled: true }, + }, + }); + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(screen.getByTestId('attach-file-menu')).toBeInTheDocument(); + }); + + it('renders null for assistants endpoint when fileConfig.assistants.disabled is true', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.assistants]: { disabled: true }, + }, + }); + const { container } = renderComponent({ + endpoint: EModelEndpoint.assistants, + }); + expect(container.innerHTML).toBe(''); + }); + }); + describe('endpointFileConfig resolution', () => { it('passes Moonshot-specific file config for agent with Moonshot provider', () => { mockAgentsMap = { diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx index aeb0dd3ff9..2bbd3fea22 100644 --- a/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx @@ -18,7 +18,7 @@ const mockEndpointsConfig: TEndpointsConfig = { 'Some Endpoint': { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, }; -let mockFileConfig = mergeFileConfig({ +const defaultFileConfig = mergeFileConfig({ endpoints: { Moonshot: { fileLimit: 5 }, [EModelEndpoint.agents]: { fileLimit: 20 }, @@ -26,6 +26,8 @@ let mockFileConfig = mergeFileConfig({ }, }); +let mockFileConfig = defaultFileConfig; + jest.mock('~/data-provider', () => ({ useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), useGetFileConfig: ({ select }: { select?: (data: unknown) => unknown }) => ({ @@ -118,13 +120,16 @@ describe('AgentPanel file config resolution (useAgentFileConfig)', () => { }); describe('disabled state', () => { + beforeEach(() => { + mockFileConfig = defaultFileConfig; + }); + it('reports not disabled for standard config', () => { render(); expect(screen.getByTestId('disabled').textContent).toBe('false'); }); it('reports disabled when provider-specific config is disabled', () => { - const original = mockFileConfig; mockFileConfig = mergeFileConfig({ endpoints: { Moonshot: { disabled: true }, @@ -135,8 +140,44 @@ describe('AgentPanel file config resolution (useAgentFileConfig)', () => { render(); expect(screen.getByTestId('disabled').textContent).toBe('true'); + }); - mockFileConfig = original; + it('reports disabled when agents config is disabled and no provider set', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { disabled: true }, + default: { fileLimit: 10 }, + }, + }); + + render(); + expect(screen.getByTestId('disabled').textContent).toBe('true'); + }); + + it('reports disabled when agents is disabled and provider has no specific config', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { disabled: true }, + default: { fileLimit: 10 }, + }, + }); + + render(); + expect(screen.getByTestId('disabled').textContent).toBe('true'); + }); + + it('provider-specific enabled overrides agents disabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { disabled: false, fileLimit: 5 }, + [EModelEndpoint.agents]: { disabled: true }, + default: { fileLimit: 10 }, + }, + }); + + render(); + expect(screen.getByTestId('disabled').textContent).toBe('false'); + expect(screen.getByTestId('fileLimit').textContent).toBe('5'); }); }); From e079fc4900113711a704933e3059fdf215e50317 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 10:39:42 -0400 Subject: [PATCH 037/111] =?UTF-8?q?=F0=9F=93=8E=20fix:=20Enforce=20File=20?= =?UTF-8?q?Count=20and=20Size=20Limits=20Across=20All=20Attachment=20Paths?= =?UTF-8?q?=20(#12239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: Enforce fileLimit and totalSizeLimit in Attached Files panel The Files side panel (PanelTable) was not checking fileLimit or totalSizeLimit from fileConfig when attaching previously uploaded files, allowing users to bypass per-endpoint file count and total size limits. * 🔧 fix: Address review findings on file limit enforcement - Fix totalSizeLimit double-counting size of already-attached files - Clarify fileLimit error message: "File limit reached: N files (endpoint)" - Replace Array.from(...).reduce with for...of loop to avoid intermediate allocation - Extract inline `type TFile` into standalone `import type` per project conventions * ✅ test: Add PanelTable handleFileClick file limit tests Cover fileLimit guard, totalSizeLimit guard, passing case, double-count prevention for re-attached files, and boundary case. * 🔧 test: Harden PanelTable test mock setup - Use explicit endpoint key matching mockConversation.endpoint instead of relying on default fallback behavior - Add supportedMimeTypes to mock config for explicit MIME coverage - Throw on missing filename cell in clickFilenameCell to prevent silent false-positive blocking assertions * ♻️ refactor: Align file validation ordering and messaging across upload paths - Reorder handleFileClick checks to match validateFiles: disabled → fileLimit → fileSizeLimit → checkType → totalSizeLimit - Change fileSizeLimit comparison from > to >= in handleFileClick to match validateFiles behavior - Align validateFiles error strings with localized key wording: "File limit reached:", "File size limit exceeded:", etc. - Remove stray console.log in validateFiles MIME-type check * ✅ test: Add validateFiles unit tests for both paths' consistency 13 tests covering disabled, empty, fileLimit (reject + boundary), fileSizeLimit (>= at limit + under limit), checkType, totalSizeLimit (reject + at limit), duplicate detection, and check ordering. Ensures both validateFiles and handleFileClick enforce the same validation rules in the same order. --- .../components/SidePanel/Files/PanelTable.tsx | 38 ++- .../Files/__tests__/PanelTable.spec.tsx | 239 ++++++++++++++++++ client/src/locales/en/translation.json | 2 + .../src/utils/__tests__/validateFiles.spec.ts | 172 +++++++++++++ client/src/utils/files.ts | 9 +- 5 files changed, 448 insertions(+), 12 deletions(-) create mode 100644 client/src/components/SidePanel/Files/__tests__/PanelTable.spec.tsx create mode 100644 client/src/utils/__tests__/validateFiles.spec.ts diff --git a/client/src/components/SidePanel/Files/PanelTable.tsx b/client/src/components/SidePanel/Files/PanelTable.tsx index 2fc8f7031b..e67e16abdd 100644 --- a/client/src/components/SidePanel/Files/PanelTable.tsx +++ b/client/src/components/SidePanel/Files/PanelTable.tsx @@ -24,14 +24,14 @@ import { type ColumnFiltersState, } from '@tanstack/react-table'; import { - fileConfig as defaultFileConfig, - checkOpenAIStorage, - mergeFileConfig, megabyte, + mergeFileConfig, + checkOpenAIStorage, isAssistantsEndpoint, getEndpointFileConfig, - type TFile, + fileConfig as defaultFileConfig, } from 'librechat-data-provider'; +import type { TFile } from 'librechat-data-provider'; import { MyFilesModal } from '~/components/Chat/Input/Files/MyFilesModal'; import { useFileMapContext, useChatContext } from '~/Providers'; import { useLocalize, useUpdateFiles } from '~/hooks'; @@ -86,7 +86,7 @@ export default function DataTable({ columns, data }: DataTablePro const fileMap = useFileMapContext(); const { showToast } = useToastContext(); - const { setFiles, conversation } = useChatContext(); + const { files, setFiles, conversation } = useChatContext(); const { data: fileConfig = null } = useGetFileConfig({ select: (data) => mergeFileConfig(data), }); @@ -142,7 +142,15 @@ export default function DataTable({ columns, data }: DataTablePro return; } - if (fileData.bytes > (endpointFileConfig.fileSizeLimit ?? Number.MAX_SAFE_INTEGER)) { + if (endpointFileConfig.fileLimit && files.size >= endpointFileConfig.fileLimit) { + showToast({ + message: `${localize('com_ui_attach_error_limit')} ${endpointFileConfig.fileLimit} files (${endpoint})`, + status: 'error', + }); + return; + } + + if (fileData.bytes >= (endpointFileConfig.fileSizeLimit ?? Number.MAX_SAFE_INTEGER)) { showToast({ message: `${localize('com_ui_attach_error_size')} ${ (endpointFileConfig.fileSizeLimit ?? 0) / megabyte @@ -160,6 +168,22 @@ export default function DataTable({ columns, data }: DataTablePro return; } + if (endpointFileConfig.totalSizeLimit) { + const existing = files.get(fileData.file_id); + let currentTotalSize = 0; + for (const f of files.values()) { + currentTotalSize += f.size; + } + currentTotalSize -= existing?.size ?? 0; + if (currentTotalSize + fileData.bytes > endpointFileConfig.totalSizeLimit) { + showToast({ + message: `${localize('com_ui_attach_error_total_size')} ${endpointFileConfig.totalSizeLimit / megabyte} MB (${endpoint})`, + status: 'error', + }); + return; + } + } + addFile({ progress: 1, attached: true, @@ -175,7 +199,7 @@ export default function DataTable({ columns, data }: DataTablePro metadata: fileData.metadata, }); }, - [addFile, fileMap, conversation, localize, showToast, fileConfig], + [addFile, files, fileMap, conversation, localize, showToast, fileConfig], ); const filenameFilter = table.getColumn('filename')?.getFilterValue() as string; diff --git a/client/src/components/SidePanel/Files/__tests__/PanelTable.spec.tsx b/client/src/components/SidePanel/Files/__tests__/PanelTable.spec.tsx new file mode 100644 index 0000000000..2639d3c100 --- /dev/null +++ b/client/src/components/SidePanel/Files/__tests__/PanelTable.spec.tsx @@ -0,0 +1,239 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { FileSources } from 'librechat-data-provider'; +import type { TFile } from 'librechat-data-provider'; +import type { ExtendedFile } from '~/common'; +import DataTable from '../PanelTable'; +import { columns } from '../PanelColumns'; + +const mockShowToast = jest.fn(); +const mockAddFile = jest.fn(); + +let mockFileMap: Record = {}; +let mockFiles: Map = new Map(); +let mockConversation: Record | null = { endpoint: 'openAI' }; +let mockRawFileConfig: Record | null = { + endpoints: { + openAI: { fileLimit: 10, supportedMimeTypes: ['application/pdf', 'text/plain'] }, + }, +}; + +jest.mock('@librechat/client', () => ({ + Table: ({ children, ...props }: { children: React.ReactNode }) => ( + {children}
+ ), + Button: ({ + children, + ...props + }: { children: React.ReactNode } & React.ButtonHTMLAttributes) => ( + + ), + TableRow: ({ children, ...props }: { children: React.ReactNode }) => ( + {children} + ), + TableHead: ({ children, ...props }: { children: React.ReactNode }) => ( + {children} + ), + TableBody: ({ children, ...props }: { children: React.ReactNode }) => ( + {children} + ), + TableCell: ({ + children, + ...props + }: { children: React.ReactNode } & React.TdHTMLAttributes) => ( + {children} + ), + FilterInput: () => , + TableHeader: ({ children, ...props }: { children: React.ReactNode }) => ( + {children} + ), + useToastContext: () => ({ showToast: mockShowToast }), +})); + +jest.mock('~/Providers', () => ({ + useFileMapContext: () => mockFileMap, + useChatContext: () => ({ + files: mockFiles, + setFiles: jest.fn(), + conversation: mockConversation, + }), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, + useUpdateFiles: () => ({ addFile: mockAddFile }), +})); + +jest.mock('~/data-provider', () => ({ + useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({ + data: select != null ? select(mockRawFileConfig) : mockRawFileConfig, + }), +})); + +jest.mock('~/components/Chat/Input/Files/MyFilesModal', () => ({ + MyFilesModal: () => null, +})); + +jest.mock('../PanelFileCell', () => ({ row }: { row: { original: TFile } }) => ( + {row.original?.filename} +)); + +function makeFile(overrides: Partial = {}): TFile { + return { + user: 'user-1', + file_id: 'file-1', + bytes: 1024, + embedded: false, + filename: 'test.pdf', + filepath: '/files/test.pdf', + object: 'file', + type: 'application/pdf', + usage: 0, + source: FileSources.local, + ...overrides, + }; +} + +function makeExtendedFile(overrides: Partial = {}): ExtendedFile { + return { + file_id: 'ext-1', + size: 1024, + progress: 1, + source: FileSources.local, + ...overrides, + }; +} + +function renderTable(data: TFile[]) { + return render(); +} + +function clickFilenameCell() { + const cells = screen.getAllByRole('button'); + const filenameCell = cells.find( + (cell) => cell.tagName === 'TD' && cell.textContent && !cell.textContent.includes('com_ui_'), + ); + if (!filenameCell) { + throw new Error('Could not find filename cell with role="button" — check mock setup'); + } + fireEvent.click(filenameCell); + return filenameCell; +} + +describe('PanelTable handleFileClick', () => { + beforeEach(() => { + mockShowToast.mockClear(); + mockAddFile.mockClear(); + mockFiles = new Map(); + mockConversation = { endpoint: 'openAI' }; + mockRawFileConfig = { + endpoints: { + openAI: { + fileLimit: 5, + totalSizeLimit: 10, + supportedMimeTypes: ['application/pdf', 'text/plain'], + }, + }, + }; + }); + + it('calls addFile when within file limits', () => { + const file = makeFile(); + mockFileMap = { [file.file_id]: file }; + + renderTable([file]); + clickFilenameCell(); + + expect(mockAddFile).toHaveBeenCalledTimes(1); + expect(mockAddFile).toHaveBeenCalledWith( + expect.objectContaining({ + file_id: file.file_id, + attached: true, + progress: 1, + }), + ); + expect(mockShowToast).not.toHaveBeenCalledWith(expect.objectContaining({ status: 'error' })); + }); + + it('blocks attachment when fileLimit is reached', () => { + const file = makeFile({ file_id: 'new-file', filename: 'new.pdf' }); + mockFileMap = { [file.file_id]: file }; + + mockFiles = new Map( + Array.from({ length: 5 }, (_, i) => [ + `existing-${i}`, + makeExtendedFile({ file_id: `existing-${i}` }), + ]), + ); + + renderTable([file]); + clickFilenameCell(); + + expect(mockAddFile).not.toHaveBeenCalled(); + expect(mockShowToast).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('com_ui_attach_error_limit'), + status: 'error', + }), + ); + }); + + it('blocks attachment when totalSizeLimit would be exceeded', () => { + const MB = 1024 * 1024; + const largeFile = makeFile({ file_id: 'large-file', bytes: 6 * MB }); + mockFileMap = { [largeFile.file_id]: largeFile }; + + mockFiles = new Map([ + ['existing-1', makeExtendedFile({ file_id: 'existing-1', size: 5 * MB })], + ]); + + renderTable([largeFile]); + clickFilenameCell(); + + expect(mockAddFile).not.toHaveBeenCalled(); + expect(mockShowToast).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('com_ui_attach_error_total_size'), + status: 'error', + }), + ); + }); + + it('does not double-count size of already-attached file', () => { + const MB = 1024 * 1024; + const file = makeFile({ file_id: 'reattach', bytes: 5 * MB }); + mockFileMap = { [file.file_id]: file }; + + mockFiles = new Map([ + ['reattach', makeExtendedFile({ file_id: 'reattach', size: 5 * MB })], + ['other', makeExtendedFile({ file_id: 'other', size: 4 * MB })], + ]); + + renderTable([file]); + clickFilenameCell(); + + expect(mockAddFile).toHaveBeenCalledTimes(1); + expect(mockShowToast).not.toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('com_ui_attach_error_total_size'), + }), + ); + }); + + it('allows attachment when just under fileLimit', () => { + const file = makeFile({ file_id: 'under-limit' }); + mockFileMap = { [file.file_id]: file }; + + mockFiles = new Map( + Array.from({ length: 4 }, (_, i) => [ + `existing-${i}`, + makeExtendedFile({ file_id: `existing-${i}` }), + ]), + ); + + renderTable([file]); + clickFilenameCell(); + + expect(mockAddFile).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 196ea2ad4a..f45cdd5f8c 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -748,7 +748,9 @@ "com_ui_attach_error": "Cannot attach file. Create or select a conversation, or try refreshing the page.", "com_ui_attach_error_disabled": "File uploads are disabled for this endpoint", "com_ui_attach_error_openai": "Cannot attach Assistant files to other endpoints", + "com_ui_attach_error_limit": "File limit reached:", "com_ui_attach_error_size": "File size limit exceeded for endpoint:", + "com_ui_attach_error_total_size": "Total file size limit exceeded for endpoint:", "com_ui_attach_error_type": "Unsupported file type for endpoint:", "com_ui_attach_remove": "Remove file", "com_ui_attach_warn_endpoint": "Non-Assistant files may be ignored without a compatible tool", diff --git a/client/src/utils/__tests__/validateFiles.spec.ts b/client/src/utils/__tests__/validateFiles.spec.ts new file mode 100644 index 0000000000..6d690bf62a --- /dev/null +++ b/client/src/utils/__tests__/validateFiles.spec.ts @@ -0,0 +1,172 @@ +import { megabyte, fileConfig as defaultFileConfig } from 'librechat-data-provider'; +import type { EndpointFileConfig, FileConfig } from 'librechat-data-provider'; +import type { ExtendedFile } from '~/common'; +import { validateFiles } from '../files'; + +const supportedMimeTypes = defaultFileConfig.endpoints.default.supportedMimeTypes; + +function makeEndpointConfig(overrides: Partial = {}): EndpointFileConfig { + return { + fileLimit: 10, + fileSizeLimit: 25 * megabyte, + totalSizeLimit: 100 * megabyte, + supportedMimeTypes, + disabled: false, + ...overrides, + }; +} + +function makeFile(name: string, type: string, size: number): File { + const content = new ArrayBuffer(size); + return new File([content], name, { type }); +} + +function makeExtendedFile(overrides: Partial = {}): ExtendedFile { + return { + file_id: 'ext-1', + size: 1024, + progress: 1, + type: 'application/pdf', + ...overrides, + }; +} + +describe('validateFiles', () => { + let setError: jest.Mock; + let files: Map; + let endpointFileConfig: EndpointFileConfig; + const fileConfig: FileConfig | null = null; + + beforeEach(() => { + setError = jest.fn(); + files = new Map(); + endpointFileConfig = makeEndpointConfig(); + }); + + it('returns true when all checks pass', () => { + const fileList = [makeFile('doc.pdf', 'application/pdf', 1024)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(true); + expect(setError).not.toHaveBeenCalled(); + }); + + it('rejects when endpoint is disabled', () => { + endpointFileConfig = makeEndpointConfig({ disabled: true }); + const fileList = [makeFile('doc.pdf', 'application/pdf', 1024)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(false); + expect(setError).toHaveBeenCalledWith('com_ui_attach_error_disabled'); + }); + + it('rejects empty files (zero bytes)', () => { + const fileList = [makeFile('empty.pdf', 'application/pdf', 0)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(false); + expect(setError).toHaveBeenCalledWith('com_error_files_empty'); + }); + + it('rejects when fileLimit would be exceeded', () => { + endpointFileConfig = makeEndpointConfig({ fileLimit: 3 }); + files = new Map([ + ['f1', makeExtendedFile({ file_id: 'f1', filename: 'one.pdf', size: 2048 })], + ['f2', makeExtendedFile({ file_id: 'f2', filename: 'two.pdf', size: 3072 })], + ]); + const fileList = [ + makeFile('a.pdf', 'application/pdf', 1024), + makeFile('b.pdf', 'application/pdf', 2048), + ]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(false); + expect(setError).toHaveBeenCalledWith('File limit reached: 3 files'); + }); + + it('allows upload when exactly at fileLimit boundary', () => { + endpointFileConfig = makeEndpointConfig({ fileLimit: 3 }); + files = new Map([ + ['f1', makeExtendedFile({ file_id: 'f1', filename: 'one.pdf', size: 2048 })], + ['f2', makeExtendedFile({ file_id: 'f2', filename: 'two.pdf', size: 3072 })], + ]); + const fileList = [makeFile('a.pdf', 'application/pdf', 1024)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(true); + }); + + it('rejects unsupported MIME type', () => { + const fileList = [makeFile('data.xyz', 'application/x-unknown', 1024)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(false); + expect(setError).toHaveBeenCalledWith('Unsupported file type: application/x-unknown'); + }); + + it('rejects when file size equals fileSizeLimit (>= comparison)', () => { + const limit = 5 * megabyte; + endpointFileConfig = makeEndpointConfig({ fileSizeLimit: limit }); + const fileList = [makeFile('exact.pdf', 'application/pdf', limit)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(false); + expect(setError).toHaveBeenCalledWith(`File size limit exceeded: ${limit / megabyte} MB`); + }); + + it('allows file just under fileSizeLimit', () => { + const limit = 5 * megabyte; + endpointFileConfig = makeEndpointConfig({ fileSizeLimit: limit }); + const fileList = [makeFile('under.pdf', 'application/pdf', limit - 1)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(true); + }); + + it('rejects when totalSizeLimit would be exceeded', () => { + const limit = 10 * megabyte; + endpointFileConfig = makeEndpointConfig({ totalSizeLimit: limit }); + files = new Map([['f1', makeExtendedFile({ file_id: 'f1', size: 6 * megabyte })]]); + const fileList = [makeFile('big.pdf', 'application/pdf', 5 * megabyte)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(false); + expect(setError).toHaveBeenCalledWith(`Total file size limit exceeded: ${limit / megabyte} MB`); + }); + + it('allows when totalSizeLimit is exactly met', () => { + const limit = 10 * megabyte; + endpointFileConfig = makeEndpointConfig({ totalSizeLimit: limit }); + files = new Map([['f1', makeExtendedFile({ file_id: 'f1', size: 5 * megabyte })]]); + const fileList = [makeFile('fits.pdf', 'application/pdf', 5 * megabyte)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(true); + }); + + it('rejects duplicate files', () => { + files = new Map([ + [ + 'f1', + makeExtendedFile({ + file_id: 'f1', + file: makeFile('doc.pdf', 'application/pdf', 1024), + filename: 'doc.pdf', + size: 1024, + type: 'application/pdf', + }), + ], + ]); + const fileList = [makeFile('doc.pdf', 'application/pdf', 1024)]; + const result = validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(result).toBe(false); + expect(setError).toHaveBeenCalledWith('com_error_files_dupe'); + }); + + it('enforces check ordering: disabled before fileLimit', () => { + endpointFileConfig = makeEndpointConfig({ disabled: true, fileLimit: 1 }); + files = new Map([['f1', makeExtendedFile({ file_id: 'f1', filename: 'existing.pdf' })]]); + const fileList = [makeFile('doc.pdf', 'application/pdf', 1024)]; + validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(setError).toHaveBeenCalledWith('com_ui_attach_error_disabled'); + }); + + it('enforces check ordering: fileLimit before fileSizeLimit', () => { + const limit = 1; + endpointFileConfig = makeEndpointConfig({ fileLimit: 1, fileSizeLimit: limit }); + files = new Map([['f1', makeExtendedFile({ file_id: 'f1', filename: 'existing.pdf' })]]); + const fileList = [makeFile('huge.pdf', 'application/pdf', limit)]; + validateFiles({ files, fileList, setError, endpointFileConfig, fileConfig }); + expect(setError).toHaveBeenCalledWith('File limit reached: 1 files'); + }); +}); diff --git a/client/src/utils/files.ts b/client/src/utils/files.ts index b4d362d456..be81a31b79 100644 --- a/client/src/utils/files.ts +++ b/client/src/utils/files.ts @@ -251,7 +251,7 @@ export const validateFiles = ({ const currentTotalSize = existingFiles.reduce((total, file) => total + file.size, 0); if (fileLimit && fileList.length + files.size > fileLimit) { - setError(`You can only upload up to ${fileLimit} files at a time.`); + setError(`File limit reached: ${fileLimit} files`); return false; } @@ -282,19 +282,18 @@ export const validateFiles = ({ } if (!checkType(originalFile.type, mimeTypesToCheck)) { - console.log(originalFile); - setError('Currently, unsupported file type: ' + originalFile.type); + setError(`Unsupported file type: ${originalFile.type}`); return false; } if (fileSizeLimit && originalFile.size >= fileSizeLimit) { - setError(`File size exceeds ${fileSizeLimit / megabyte} MB.`); + setError(`File size limit exceeded: ${fileSizeLimit / megabyte} MB`); return false; } } if (totalSizeLimit && currentTotalSize + incomingTotalSize > totalSizeLimit) { - setError(`The total size of the files cannot exceed ${totalSizeLimit / megabyte} MB.`); + setError(`Total file size limit exceeded: ${totalSizeLimit / megabyte} MB`); return false; } From a01959b3d2eddb0961c611d429a703187d2e347b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 11:11:10 -0400 Subject: [PATCH 038/111] =?UTF-8?q?=F0=9F=9B=B0=EF=B8=8F=20fix:=20Cross-Re?= =?UTF-8?q?plica=20Created=20Event=20Delivery=20(#12231)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: emit created event from metadata on cross-replica subscribe In multi-instance Redis deployments, the created event (which triggers sidebar conversation creation) was lost when the SSE subscriber connected to a different instance than the one generating. The event was only in the generating instance's local earlyEventBuffer and the Redis pub/sub message was already gone by the time the subscriber's channel was active. When subscribing cross-replica (empty buffer, Redis mode, userMessage already in job metadata), reconstruct and emit the created event directly from stored metadata. * test: add skipBufferReplay regression guard for cross-replica created event Add test asserting the resume path (skipBufferReplay: true) does NOT emit a created event on cross-replica subscribe — prevents the duplication fix from PR #12225 from regressing. Add explanatory JSDoc on the cross-replica fallback branch documenting which fields are preserved from trackUserMessage() and why sender/isCreatedByUser are hardcoded. * refactor: replace as-unknown-as casts with discriminated ServerSentEvent union Split ServerSentEvent into StreamEvent | CreatedEvent | FinalEvent so event shapes are statically typed. Removes all as-unknown-as casts in GenerationJobManager and test file; narrows with proper union members where properties are accessed. * fix: await trackUserMessage before PUBLISH for structural ordering trackUserMessage was fire-and-forget — the HSET for userMessage could theoretically race with the PUBLISH. Await it so the write commits before the pub/sub fires, guaranteeing any cross-replica getJob() after the pub/sub window always finds userMessage in Redis. No-op for non-created events (early return before any async work). * refactor: type CreatedEvent.message explicitly, fix JSDoc and import Give CreatedEvent.message its full known shape instead of Record. Update sendEvent JSDoc to reflect the discriminated union. Use barrel import in test file. * refactor: type FinalEvent fields with explicit message and conversation shapes Replace Record on requestMessage, responseMessage, conversation, and runMessages with FinalMessageFields and a typed conversation shape. Captures the known field set used by all final event constructors (abort handler in GenerationJobManager and normal completion in request.js) while allowing extension via index signature for fields contributed by the full TMessage/TConversation schemas. * refactor: narrow trackUserMessage with discriminated union, disambiguate error fields Use 'created' in event to narrow ServerSentEvent to CreatedEvent, eliminating all Record casts and manual field assertions. Add JSDoc to the two distinct error fields on FinalMessageFields and FinalEvent to prevent confusion. * fix: update cross-replica test to expect created event from metadata The cross-replica subscribe fallback now correctly emits a created event reconstructed from persisted metadata when userMessage exists in the Redis job hash. Replica B receives 4 events (created + 3 deltas) instead of 3. --- .../api/src/stream/GenerationJobManager.ts | 50 ++++-- ...ationJobManager.stream_integration.spec.ts | 142 ++++++++++++++++-- packages/api/src/types/events.ts | 49 +++++- packages/api/src/utils/events.ts | 9 +- 4 files changed, 218 insertions(+), 32 deletions(-) diff --git a/packages/api/src/stream/GenerationJobManager.ts b/packages/api/src/stream/GenerationJobManager.ts index 1b612dcb8f..3e04ab734b 100644 --- a/packages/api/src/stream/GenerationJobManager.ts +++ b/packages/api/src/stream/GenerationJobManager.ts @@ -656,7 +656,7 @@ class GenerationJobManagerClass { aborted: true, // Flag for early abort - no messages saved, frontend should go to new chat earlyAbort: isEarlyAbort, - } as unknown as t.ServerSentEvent; + } satisfies t.FinalEvent as t.ServerSentEvent; if (runtime) { runtime.finalEvent = abortFinalEvent; @@ -781,6 +781,27 @@ class GenerationJobManagerClass { } } runtime.earlyEventBuffer = []; + } else if (this._isRedis && !options?.skipBufferReplay && jobData?.userMessage) { + /** + * Cross-replica fallback: the created event was buffered on the generating + * instance and published via Redis pub/sub before this subscriber was active. + * Reconstruct from persisted metadata. Only fields stored by trackUserMessage() + * are available (messageId, parentMessageId, conversationId, text); + * sender/isCreatedByUser are invariant for user messages and added back here. + */ + logger.debug( + `[GenerationJobManager] Cross-replica subscribe: emitting created event from metadata for ${streamId}`, + ); + const createdEvent: t.CreatedEvent = { + created: true, + message: { + ...jobData.userMessage, + sender: 'User', + isCreatedByUser: true, + }, + streamId, + }; + onChunk(createdEvent); } this.eventTransport.syncReorderBuffer?.(streamId); @@ -858,8 +879,7 @@ class GenerationJobManagerClass { return; } - // Track user message from created event - this.trackUserMessage(streamId, event); + await this.trackUserMessage(streamId, event); // For Redis mode, persist chunk for later reconstruction (fire-and-forget for resumability) if (this._isRedis) { @@ -943,29 +963,31 @@ class GenerationJobManagerClass { } /** - * Track user message from created event. + * Persist user message metadata from the created event. + * Awaited in emitChunk so the HSET commits before the PUBLISH, + * guaranteeing any cross-replica getJob() after the pub/sub window + * finds userMessage in Redis. */ - private trackUserMessage(streamId: string, event: t.ServerSentEvent): void { - const data = event as Record; - if (!data.created || !data.message) { + private async trackUserMessage(streamId: string, event: t.ServerSentEvent): Promise { + if (!('created' in event)) { return; } - const message = data.message as Record; + const { message } = event; const updates: Partial = { userMessage: { - messageId: message.messageId as string, - parentMessageId: message.parentMessageId as string | undefined, - conversationId: message.conversationId as string | undefined, - text: message.text as string | undefined, + messageId: message.messageId, + parentMessageId: message.parentMessageId, + conversationId: message.conversationId, + text: message.text, }, }; if (message.conversationId) { - updates.conversationId = message.conversationId as string; + updates.conversationId = message.conversationId; } - this.jobStore.updateJob(streamId, updates); + await this.jobStore.updateJob(streamId, updates); } /** diff --git a/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts b/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts index 2f23510018..3e85ace56d 100644 --- a/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts +++ b/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts @@ -1,6 +1,6 @@ /* eslint jest/no-standalone-expect: ["error", { "additionalTestBlockFunctions": ["testRedis"] }] */ import type { Redis, Cluster } from 'ioredis'; -import type { ServerSentEvent } from '~/types/events'; +import type { ServerSentEvent, StreamEvent, CreatedEvent } from '~/types'; import { InMemoryEventTransport } from '~/stream/implementations/InMemoryEventTransport'; import { RedisEventTransport } from '~/stream/implementations/RedisEventTransport'; import { InMemoryJobStore } from '~/stream/implementations/InMemoryJobStore'; @@ -771,6 +771,127 @@ describe('GenerationJobManager Integration Tests', () => { await GenerationJobManager.destroy(); await jobStore.destroy(); }); + + test('should emit created event from metadata on cross-replica subscribe', async () => { + const replicaAJobStore = new RedisJobStore(ioredisClient!); + await replicaAJobStore.initialize(); + + const streamId = `cross-created-${Date.now()}`; + const userId = 'test-user'; + + await replicaAJobStore.createJob(streamId, userId); + await replicaAJobStore.updateJob(streamId, { + userMessage: { + messageId: 'msg-123', + parentMessageId: '00000000-0000-0000-0000-000000000000', + conversationId: streamId, + text: 'hello world', + }, + }); + + jest.resetModules(); + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + GenerationJobManager.configure(services); + GenerationJobManager.initialize(); + + const received: unknown[] = []; + const subscription = await GenerationJobManager.subscribe( + streamId, + (event) => received.push(event), + ); + + expect(subscription).not.toBeNull(); + expect(received.length).toBe(1); + + const created = received[0] as CreatedEvent; + expect(created.created).toBe(true); + expect(created.streamId).toBe(streamId); + expect(created.message.messageId).toBe('msg-123'); + expect(created.message.conversationId).toBe(streamId); + expect(created.message.sender).toBe('User'); + expect(created.message.isCreatedByUser).toBe(true); + + subscription?.unsubscribe(); + await GenerationJobManager.destroy(); + await replicaAJobStore.destroy(); + }); + + test('should NOT emit created event from metadata when userMessage is not set', async () => { + const replicaAJobStore = new RedisJobStore(ioredisClient!); + await replicaAJobStore.initialize(); + + const streamId = `cross-no-created-${Date.now()}`; + await replicaAJobStore.createJob(streamId, 'test-user'); + + jest.resetModules(); + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + GenerationJobManager.configure(services); + GenerationJobManager.initialize(); + + const received: unknown[] = []; + const subscription = await GenerationJobManager.subscribe( + streamId, + (event) => received.push(event), + ); + + expect(subscription).not.toBeNull(); + expect(received.length).toBe(0); + + subscription?.unsubscribe(); + await GenerationJobManager.destroy(); + await replicaAJobStore.destroy(); + }); + + test('should NOT emit created event when skipBufferReplay is true (resume path)', async () => { + const replicaAJobStore = new RedisJobStore(ioredisClient!); + await replicaAJobStore.initialize(); + + const streamId = `cross-no-replay-${Date.now()}`; + await replicaAJobStore.createJob(streamId, 'test-user'); + await replicaAJobStore.updateJob(streamId, { + userMessage: { + messageId: 'msg-456', + conversationId: streamId, + text: 'hi', + }, + }); + + jest.resetModules(); + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + GenerationJobManager.configure(services); + GenerationJobManager.initialize(); + + const received: unknown[] = []; + const subscription = await GenerationJobManager.subscribe( + streamId, + (event) => received.push(event), + undefined, + undefined, + { skipBufferReplay: true }, + ); + + expect(subscription).not.toBeNull(); + expect(received.length).toBe(0); + + subscription?.unsubscribe(); + await GenerationJobManager.destroy(); + await replicaAJobStore.destroy(); + }); }); describeRedis('Sequential Event Ordering (Redis)', () => { @@ -1040,7 +1161,7 @@ describe('GenerationJobManager Integration Tests', () => { created: true, message: { text: 'hello' }, streamId, - } as unknown as ServerSentEvent); + } as CreatedEvent); await manager.emitChunk(streamId, { event: 'on_message_delta', data: { delta: { content: { type: 'text', text: 'First chunk' } } }, @@ -1077,7 +1198,7 @@ describe('GenerationJobManager Integration Tests', () => { created: true, message: { text: 'hello' }, streamId, - } as unknown as ServerSentEvent); + } as CreatedEvent); await manager.emitChunk(streamId, { event: 'on_message_delta', data: { delta: { content: { type: 'text', text: 'First' } } }, @@ -1123,7 +1244,7 @@ describe('GenerationJobManager Integration Tests', () => { created: true, message: { text: 'hello' }, streamId, - } as unknown as ServerSentEvent); + } as CreatedEvent); await manager.emitChunk(streamId, { event: 'on_run_step', data: { id: 'step-1', type: 'message_creation', index: 0 }, @@ -1228,7 +1349,7 @@ describe('GenerationJobManager Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 20)); expect(resumeEvents.length).toBe(1); - expect(resumeEvents[0].event).toBe('on_message_delta'); + expect((resumeEvents[0] as StreamEvent).event).toBe('on_message_delta'); sub2?.unsubscribe(); await manager.destroy(); @@ -1262,7 +1383,7 @@ describe('GenerationJobManager Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 20)); expect(sub2Events.length).toBe(1); - expect(sub2Events[0].event).toBe('on_message_delta'); + expect((sub2Events[0] as StreamEvent).event).toBe('on_message_delta'); sub2?.unsubscribe(); await manager.destroy(); @@ -1427,7 +1548,7 @@ describe('GenerationJobManager Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 200)); expect(resumeEvents.length).toBe(1); - expect(resumeEvents[0].event).toBe('on_message_delta'); + expect((resumeEvents[0] as StreamEvent).event).toBe('on_message_delta'); sub2?.unsubscribe(); await manager.destroy(); @@ -1458,7 +1579,7 @@ describe('GenerationJobManager Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 200)); expect(sub2Events.length).toBe(1); - expect(sub2Events[0].event).toBe('on_message_delta'); + expect((sub2Events[0] as StreamEvent).event).toBe('on_message_delta'); sub2?.unsubscribe(); await manager.destroy(); @@ -1997,7 +2118,7 @@ describe('GenerationJobManager Integration Tests', () => { created: true, message: { text: 'hello' }, streamId, - } as unknown as ServerSentEvent); + } as CreatedEvent); const receivedOnA: unknown[] = []; const subA = await replicaA.subscribe(streamId, (event: unknown) => receivedOnA.push(event)); @@ -2035,7 +2156,8 @@ describe('GenerationJobManager Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 700)); expect(receivedOnA.length).toBe(4); - expect(receivedOnB.length).toBe(3); + expect(receivedOnB.length).toBe(4); + expect((receivedOnB[0] as CreatedEvent).created).toBe(true); subA?.unsubscribe(); subB?.unsubscribe(); diff --git a/packages/api/src/types/events.ts b/packages/api/src/types/events.ts index 1e866fa840..d068888b17 100644 --- a/packages/api/src/types/events.ts +++ b/packages/api/src/types/events.ts @@ -1,4 +1,49 @@ -export type ServerSentEvent = { +/** SSE streaming event (on_run_step, on_message_delta, etc.) */ +export type StreamEvent = { + event: string; data: string | Record; - event?: string; }; + +/** Control event emitted when user message is created and generation starts */ +export type CreatedEvent = { + created: true; + message: { + messageId: string; + parentMessageId?: string; + conversationId?: string; + text?: string; + sender: string; + isCreatedByUser: boolean; + }; + streamId: string; +}; + +export type FinalMessageFields = { + messageId?: string; + parentMessageId?: string; + conversationId?: string; + text?: string; + content?: unknown[]; + sender?: string; + isCreatedByUser?: boolean; + unfinished?: boolean; + /** Per-message error flag — matches TMessage.error (boolean or error text) */ + error?: boolean | string; + [key: string]: unknown; +}; + +/** Terminal event emitted when generation completes or is aborted */ +export type FinalEvent = { + final: true; + requestMessage?: FinalMessageFields | null; + responseMessage?: FinalMessageFields | null; + conversation?: { conversationId?: string; [key: string]: unknown } | null; + title?: string; + aborted?: boolean; + earlyAbort?: boolean; + runMessages?: FinalMessageFields[]; + /** Top-level event error (abort-during-completion edge case) */ + error?: { message: string }; +}; + +export type ServerSentEvent = StreamEvent | CreatedEvent | FinalEvent; diff --git a/packages/api/src/utils/events.ts b/packages/api/src/utils/events.ts index 20c9583993..e084e631f5 100644 --- a/packages/api/src/utils/events.ts +++ b/packages/api/src/utils/events.ts @@ -2,14 +2,11 @@ import type { Response as ServerResponse } from 'express'; import type { ServerSentEvent } from '~/types'; /** - * Sends message data in Server Sent Events format. - * @param res - The server response. - * @param event - The message event. - * @param event.event - The type of event. - * @param event.data - The message to be sent. + * Sends a Server-Sent Event to the client. + * Empty-string StreamEvent data is silently dropped. */ export function sendEvent(res: ServerResponse, event: ServerSentEvent): void { - if (typeof event.data === 'string' && event.data.length === 0) { + if ('data' in event && typeof event.data === 'string' && event.data.length === 0) { return; } res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`); From a0b4949a059732ee4b457be8a52d7d4015cc950b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 17:07:55 -0400 Subject: [PATCH 039/111] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20Cover=20?= =?UTF-8?q?full=20fe80::/10=20link-local=20range=20in=20IPv6=20check=20(#1?= =?UTF-8?q?2244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Cover full fe80::/10 link-local range in SSRF IPv6 check The `isPrivateIP` check used `startsWith('fe80')` which only matched fe80:: but missed fe90::–febf:: (the rest of the RFC 4291 fe80::/10 link-local block). Replace with a proper bitwise hextet check. * 🛡️ fix: Guard isIPv6LinkLocal against parseInt partial-parse on hostnames parseInt('fe90.example.com', 16) stops at the dot and returns 0xfe90, which passes the bitmask check and false-positives legitimate domains. Add colon-presence guard (IPv6 literals always contain ':') and a hex regex validation on the first hextet before parseInt. Also document why fc/fd use startsWith while fe80::/10 needs bitwise. * ✅ test: Harden IPv6 link-local SSRF tests with false-positive guards - Assert fe90/fea0/febf hostnames are NOT blocked (regression guard) - Add feb0::1 and bracket form [fe90::1] to isPrivateIP coverage - Extend resolveHostnameSSRF tests for fe90::1 and febf::1 --- packages/api/src/auth/domain.spec.ts | 25 ++++++++++++++++++++++++- packages/api/src/auth/domain.ts | 18 ++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/api/src/auth/domain.spec.ts b/packages/api/src/auth/domain.spec.ts index 9812960cd9..8ba72d82a2 100644 --- a/packages/api/src/auth/domain.spec.ts +++ b/packages/api/src/auth/domain.spec.ts @@ -177,6 +177,20 @@ describe('isSSRFTarget', () => { expect(isSSRFTarget('fd00::1')).toBe(true); expect(isSSRFTarget('fe80::1')).toBe(true); }); + + it('should block full fe80::/10 link-local range (fe80–febf)', () => { + expect(isSSRFTarget('fe90::1')).toBe(true); + expect(isSSRFTarget('fea0::1')).toBe(true); + expect(isSSRFTarget('feb0::1')).toBe(true); + expect(isSSRFTarget('febf::1')).toBe(true); + expect(isSSRFTarget('fec0::1')).toBe(false); + }); + + it('should NOT false-positive on hostnames whose first label resembles a link-local prefix', () => { + expect(isSSRFTarget('fe90.example.com')).toBe(false); + expect(isSSRFTarget('fea0.api.io')).toBe(false); + expect(isSSRFTarget('febf.service.net')).toBe(false); + }); }); describe('internal hostnames', () => { @@ -277,10 +291,17 @@ describe('isPrivateIP', () => { expect(isPrivateIP('[::1]')).toBe(true); }); - it('should detect unique local (fc/fd) and link-local (fe80)', () => { + it('should detect unique local (fc/fd) and link-local (fe80::/10)', () => { expect(isPrivateIP('fc00::1')).toBe(true); expect(isPrivateIP('fd00::1')).toBe(true); expect(isPrivateIP('fe80::1')).toBe(true); + expect(isPrivateIP('fe90::1')).toBe(true); + expect(isPrivateIP('fea0::1')).toBe(true); + expect(isPrivateIP('feb0::1')).toBe(true); + expect(isPrivateIP('febf::1')).toBe(true); + expect(isPrivateIP('[fe90::1]')).toBe(true); + expect(isPrivateIP('fec0::1')).toBe(false); + expect(isPrivateIP('fe90.example.com')).toBe(false); }); }); @@ -482,6 +503,8 @@ describe('resolveHostnameSSRF', () => { expect(await resolveHostnameSSRF('::1')).toBe(true); expect(await resolveHostnameSSRF('fc00::1')).toBe(true); expect(await resolveHostnameSSRF('fe80::1')).toBe(true); + expect(await resolveHostnameSSRF('fe90::1')).toBe(true); + expect(await resolveHostnameSSRF('febf::1')).toBe(true); expect(mockedLookup).not.toHaveBeenCalled(); }); diff --git a/packages/api/src/auth/domain.ts b/packages/api/src/auth/domain.ts index 2761a80b55..37510f5e9b 100644 --- a/packages/api/src/auth/domain.ts +++ b/packages/api/src/auth/domain.ts @@ -59,6 +59,20 @@ function isPrivateIPv4(a: number, b: number, c: number): boolean { return false; } +/** Checks if a pre-normalized (lowercase, bracket-stripped) IPv6 address falls within fe80::/10 */ +function isIPv6LinkLocal(ipv6: string): boolean { + if (!ipv6.includes(':')) { + return false; + } + const firstHextet = ipv6.split(':', 1)[0]; + if (!firstHextet || !/^[0-9a-f]{1,4}$/.test(firstHextet)) { + return false; + } + const hextet = parseInt(firstHextet, 16); + // /10 mask (0xffc0) preserves top 10 bits: fe80 = 1111_1110_10xx_xxxx + return (hextet & 0xffc0) === 0xfe80; +} + /** Checks if an IPv6 address embeds a private IPv4 via 6to4, NAT64, or Teredo */ function hasPrivateEmbeddedIPv4(ipv6: string): boolean { if (!ipv6.startsWith('2002:') && !ipv6.startsWith('64:ff9b::') && !ipv6.startsWith('2001::')) { @@ -132,9 +146,9 @@ export function isPrivateIP(ip: string): boolean { if ( normalized === '::1' || normalized === '::' || - normalized.startsWith('fc') || + normalized.startsWith('fc') || // fc00::/7 — exactly prefixes 'fc' and 'fd' normalized.startsWith('fd') || - normalized.startsWith('fe80') + isIPv6LinkLocal(normalized) // fe80::/10 — spans 0xfe80–0xfebf; bitwise check required ) { return true; } From 07d0ce4ce9885281633800d7a14d7b2abab5c76f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 17:08:43 -0400 Subject: [PATCH 040/111] =?UTF-8?q?=F0=9F=AA=A4=20fix:=20Fail-Closed=20MCP?= =?UTF-8?q?=20Domain=20Validation=20for=20Unparseable=20URLs=20(#12245)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Fail-closed MCP domain validation for unparseable URLs `isMCPDomainAllowed` returned true (allow) when `extractMCPServerDomain` could not parse the URL, treating it identically to a stdio transport. A URL containing template placeholders or invalid syntax bypassed the domain allowlist, then `processMCPEnv` resolved it to a valid—and potentially disallowed—host at connection time. Distinguish "no URL" (stdio, allowed) from "has URL but unparseable" (rejected when an allowlist is active) by checking whether `config.url` is an explicit non-empty string before falling through to the stdio path. When no allowlist is configured the guard does not fire—unparseable URLs fall through to connection-level SSRF protection via `createSSRFSafeUndiciConnect`, preserving legitimate `customUserVars` template-URL configs. * test: Expand MCP domain validation coverage for invalid/templated URLs Cover all branches of the fail-closed guard: - Invalid/templated URLs rejected when allowlist is configured - Invalid/templated URLs allowed when no allowlist (null/undefined/[]) - Whitespace-only and empty-string URLs treated as absent across all allowedDomains configurations - Stdio configs (no url property) remain allowed --- packages/api/src/auth/domain.spec.ts | 31 +++++++++++++++++++++++++++- packages/api/src/auth/domain.ts | 20 ++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/api/src/auth/domain.spec.ts b/packages/api/src/auth/domain.spec.ts index 8ba72d82a2..76f50213db 100644 --- a/packages/api/src/auth/domain.spec.ts +++ b/packages/api/src/auth/domain.spec.ts @@ -1046,8 +1046,37 @@ describe('isMCPDomainAllowed', () => { }); describe('invalid URL handling', () => { - it('should allow config with invalid URL (treated as stdio)', async () => { + it('should reject invalid URL when allowlist is configured', async () => { const config = { url: 'not-a-valid-url' }; + expect(await isMCPDomainAllowed(config, ['example.com'])).toBe(false); + }); + + it('should reject templated URL when allowlist is configured', async () => { + const config = { url: 'http://{{CUSTOM_HOST}}/mcp' }; + expect(await isMCPDomainAllowed(config, ['example.com'])).toBe(false); + }); + + it('should allow invalid URL when no allowlist is configured (defers to connection-level SSRF)', async () => { + const config = { url: 'http://{{CUSTOM_HOST}}/mcp' }; + expect(await isMCPDomainAllowed(config, null)).toBe(true); + expect(await isMCPDomainAllowed(config, undefined)).toBe(true); + expect(await isMCPDomainAllowed(config, [])).toBe(true); + }); + + it('should allow config with whitespace-only URL (treated as absent)', async () => { + const config = { url: ' ' }; + expect(await isMCPDomainAllowed(config, [])).toBe(true); + expect(await isMCPDomainAllowed(config, ['example.com'])).toBe(true); + expect(await isMCPDomainAllowed(config, null)).toBe(true); + }); + + it('should allow config with empty string URL (treated as absent)', async () => { + const config = { url: '' }; + expect(await isMCPDomainAllowed(config, ['example.com'])).toBe(true); + }); + + it('should allow config with no url property (stdio)', async () => { + const config = { command: 'node', args: ['server.js'] }; expect(await isMCPDomainAllowed(config, ['example.com'])).toBe(true); }); }); diff --git a/packages/api/src/auth/domain.ts b/packages/api/src/auth/domain.ts index 37510f5e9b..3babb09aa6 100644 --- a/packages/api/src/auth/domain.ts +++ b/packages/api/src/auth/domain.ts @@ -442,7 +442,10 @@ export async function isActionDomainAllowed( /** * Extracts full domain spec (protocol://hostname:port) from MCP server config URL. * Returns the full origin for proper protocol/port matching against allowedDomains. - * Returns null for stdio transports (no URL) or invalid URLs. + * @returns The full origin string, or null when: + * - No `url` property, non-string, or empty (stdio transport — always allowed upstream) + * - URL string present but cannot be parsed (rejected fail-closed upstream when allowlist active) + * Callers must distinguish these two null cases; see {@link isMCPDomainAllowed}. * @param config - MCP server configuration (accepts any config with optional url field) */ export function extractMCPServerDomain(config: Record): string | null { @@ -466,6 +469,11 @@ export function extractMCPServerDomain(config: Record): string * Validates MCP server domain against allowedDomains. * Supports HTTP, HTTPS, WS, and WSS protocols (per MCP specification). * Stdio transports (no URL) are always allowed. + * Configs with a non-empty URL that cannot be parsed are rejected fail-closed when an + * allowlist is active, preventing template placeholders (e.g. `{{HOST}}`) from bypassing + * domain validation after `processMCPEnv` resolves them at connection time. + * When no allowlist is configured, unparseable URLs fall through to connection-level + * SSRF protection (`createSSRFSafeUndiciConnect`). * @param config - MCP server configuration with optional url field * @param allowedDomains - List of allowed domains (with wildcard support) */ @@ -474,8 +482,16 @@ export async function isMCPDomainAllowed( allowedDomains?: string[] | null, ): Promise { const domain = extractMCPServerDomain(config); + const hasAllowlist = Array.isArray(allowedDomains) && allowedDomains.length > 0; - // Stdio transports don't have domains - always allowed + const hasExplicitUrl = + Object.hasOwn(config, 'url') && typeof config.url === 'string' && config.url.trim().length > 0; + + if (!domain && hasExplicitUrl && hasAllowlist) { + return false; + } + + // Stdio transports (no URL) are always allowed if (!domain) { return true; } From 8dc6d60750df434682a9f519ec944529c470dd7e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 17:12:45 -0400 Subject: [PATCH 041/111] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20Enforce?= =?UTF-8?q?=20MULTI=5FCONVO=20and=20agent=20ACL=20checks=20on=20addedConvo?= =?UTF-8?q?=20(#12243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Enforce MULTI_CONVO and agent ACL checks on addedConvo addedConvo.agent_id was passed through to loadAddedAgent without any permission check, enabling an authenticated user to load and execute another user's private agent via the parallel multi-convo feature. The middleware now chains a checkAddedConvoAccess gate after the primary agent check: when req.body.addedConvo is present it verifies the user has MULTI_CONVO:USE role permission, and when the addedConvo agent_id is a real (non-ephemeral) agent it runs the same canAccessResource ACL check used for the primary agent. * refactor: Harden addedConvo middleware and avoid duplicate agent fetch - Convert checkAddedConvoAccess to curried factory matching Express middleware signature: (requiredPermission) => (req, res, next) - Call checkPermission directly for the addedConvo agent instead of routing through canAccessResource's tempReq pattern; this avoids orphaning the resolved agent document and enables caching it on req.resolvedAddedAgent for downstream loadAddedAgent - Update loadAddedAgent to use req.resolvedAddedAgent when available, eliminating a duplicate getAgent DB call per chat request - Validate addedConvo is a plain object and agent_id is a string before passing to isEphemeralAgentId (prevents TypeError on object injection, returns 400-equivalent early exit instead of 500) - Fix JSDoc: "VIEW access" → "same permission as primary agent", add @param/@returns to helpers, restore @example on factory - Fix redundant return await in resolveAgentIdFromBody * test: Add canAccessAgentFromBody spec covering IDOR fix 26 integration tests using MongoMemoryServer with real models, ACL entries, and PermissionService — no mocks for core logic. Covered paths: - Factory validation (requiredPermission type check) - Primary agent: missing agent_id, ephemeral, non-agents endpoint - addedConvo absent / invalid shape (string, array, object injection) - MULTI_CONVO:USE gate: denied, missing role, ADMIN bypass - Agent resource ACL: no ACL → 403, insufficient bits → 403, nonexistent agent → 404, valid ACL → next + cached on req - End-to-end: both real agents, primary denied short-circuits, ephemeral primary + real addedConvo --- api/models/loadAddedAgent.js | 12 +- .../accessResources/canAccessAgentFromBody.js | 156 ++++-- .../canAccessAgentFromBody.spec.js | 509 ++++++++++++++++++ 3 files changed, 637 insertions(+), 40 deletions(-) create mode 100644 api/server/middleware/accessResources/canAccessAgentFromBody.spec.js diff --git a/api/models/loadAddedAgent.js b/api/models/loadAddedAgent.js index aa83375eae..101ee96685 100644 --- a/api/models/loadAddedAgent.js +++ b/api/models/loadAddedAgent.js @@ -48,14 +48,14 @@ const loadAddedAgent = async ({ req, conversation, primaryAgent }) => { return null; } - // If there's an agent_id, load the existing agent if (conversation.agent_id && !isEphemeralAgentId(conversation.agent_id)) { - if (!getAgent) { - throw new Error('getAgent not initialized - call setGetAgent first'); + let agent = req.resolvedAddedAgent; + if (!agent) { + if (!getAgent) { + throw new Error('getAgent not initialized - call setGetAgent first'); + } + agent = await getAgent({ id: conversation.agent_id }); } - const agent = await getAgent({ - id: conversation.agent_id, - }); if (!agent) { logger.warn(`[loadAddedAgent] Agent ${conversation.agent_id} not found`); diff --git a/api/server/middleware/accessResources/canAccessAgentFromBody.js b/api/server/middleware/accessResources/canAccessAgentFromBody.js index f8112af14d..572a86f5e5 100644 --- a/api/server/middleware/accessResources/canAccessAgentFromBody.js +++ b/api/server/middleware/accessResources/canAccessAgentFromBody.js @@ -1,42 +1,144 @@ const { logger } = require('@librechat/data-schemas'); const { Constants, + Permissions, ResourceType, + SystemRoles, + PermissionTypes, isAgentsEndpoint, isEphemeralAgentId, } = require('librechat-data-provider'); +const { checkPermission } = require('~/server/services/PermissionService'); const { canAccessResource } = require('./canAccessResource'); +const { getRoleByName } = require('~/models/Role'); const { getAgent } = require('~/models/Agent'); /** - * Agent ID resolver function for agent_id from request body - * Resolves custom agent ID (e.g., "agent_abc123") to MongoDB ObjectId - * This is used specifically for chat routes where agent_id comes from request body - * + * Resolves custom agent ID (e.g., "agent_abc123") to a MongoDB document. * @param {string} agentCustomId - Custom agent ID from request body - * @returns {Promise} Agent document with _id field, or null if not found + * @returns {Promise} Agent document with _id field, or null if ephemeral/not found */ const resolveAgentIdFromBody = async (agentCustomId) => { - // Handle ephemeral agents - they don't need permission checks - // Real agent IDs always start with "agent_", so anything else is ephemeral if (isEphemeralAgentId(agentCustomId)) { - return null; // No permission check needed for ephemeral agents + return null; } - - return await getAgent({ id: agentCustomId }); + return getAgent({ id: agentCustomId }); }; /** - * Middleware factory that creates middleware to check agent access permissions from request body. - * This middleware is specifically designed for chat routes where the agent_id comes from req.body - * instead of route parameters. + * Creates a `canAccessResource` middleware for the given agent ID + * and chains to the provided continuation on success. + * + * @param {string} agentId - The agent's custom string ID (e.g., "agent_abc123") + * @param {number} requiredPermission - Permission bit(s) required + * @param {import('express').Request} req + * @param {import('express').Response} res - Written on deny; continuation called on allow + * @param {Function} continuation - Called when the permission check passes + * @returns {Promise} + */ +const checkAgentResourceAccess = (agentId, requiredPermission, req, res, continuation) => { + const middleware = canAccessResource({ + resourceType: ResourceType.AGENT, + requiredPermission, + resourceIdParam: 'agent_id', + idResolver: () => resolveAgentIdFromBody(agentId), + }); + + const tempReq = { + ...req, + params: { ...req.params, agent_id: agentId }, + }; + + return middleware(tempReq, res, continuation); +}; + +/** + * Middleware factory that validates MULTI_CONVO:USE role permission and, when + * addedConvo.agent_id is a non-ephemeral agent, the same resource-level permission + * required for the primary agent (`requiredPermission`). Caches the resolved agent + * document on `req.resolvedAddedAgent` to avoid a duplicate DB fetch in `loadAddedAgent`. + * + * @param {number} requiredPermission - Permission bit(s) to check on the added agent resource + * @returns {(req: import('express').Request, res: import('express').Response, next: Function) => Promise} + */ +const checkAddedConvoAccess = (requiredPermission) => async (req, res, next) => { + const addedConvo = req.body?.addedConvo; + if (!addedConvo || typeof addedConvo !== 'object' || Array.isArray(addedConvo)) { + return next(); + } + + try { + if (!req.user?.role) { + return res.status(403).json({ + error: 'Forbidden', + message: 'Insufficient permissions for multi-conversation', + }); + } + + if (req.user.role !== SystemRoles.ADMIN) { + const role = await getRoleByName(req.user.role); + const hasMultiConvo = role?.permissions?.[PermissionTypes.MULTI_CONVO]?.[Permissions.USE]; + if (!hasMultiConvo) { + return res.status(403).json({ + error: 'Forbidden', + message: 'Multi-conversation feature is not enabled', + }); + } + } + + const addedAgentId = addedConvo.agent_id; + if (!addedAgentId || typeof addedAgentId !== 'string' || isEphemeralAgentId(addedAgentId)) { + return next(); + } + + if (req.user.role === SystemRoles.ADMIN) { + return next(); + } + + const agent = await resolveAgentIdFromBody(addedAgentId); + if (!agent) { + return res.status(404).json({ + error: 'Not Found', + message: `${ResourceType.AGENT} not found`, + }); + } + + const hasPermission = await checkPermission({ + userId: req.user.id, + role: req.user.role, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + requiredPermission, + }); + + if (!hasPermission) { + return res.status(403).json({ + error: 'Forbidden', + message: `Insufficient permissions to access this ${ResourceType.AGENT}`, + }); + } + + req.resolvedAddedAgent = agent; + return next(); + } catch (error) { + logger.error('Failed to validate addedConvo access permissions', error); + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to validate addedConvo access permissions', + }); + } +}; + +/** + * Middleware factory that checks agent access permissions from request body. + * Validates both the primary agent_id and, when present, addedConvo.agent_id + * (which also requires MULTI_CONVO:USE role permission). * * @param {Object} options - Configuration options * @param {number} options.requiredPermission - The permission bit required (1=view, 2=edit, 4=delete, 8=share) * @returns {Function} Express middleware function * * @example - * // Basic usage for agent chat (requires VIEW permission) * router.post('/chat', * canAccessAgentFromBody({ requiredPermission: PermissionBits.VIEW }), * buildEndpointOption, @@ -46,11 +148,12 @@ const resolveAgentIdFromBody = async (agentCustomId) => { const canAccessAgentFromBody = (options) => { const { requiredPermission } = options; - // Validate required options if (!requiredPermission || typeof requiredPermission !== 'number') { throw new Error('canAccessAgentFromBody: requiredPermission is required and must be a number'); } + const addedConvoMiddleware = checkAddedConvoAccess(requiredPermission); + return async (req, res, next) => { try { const { endpoint, agent_id } = req.body; @@ -67,28 +170,13 @@ const canAccessAgentFromBody = (options) => { }); } - // Skip permission checks for ephemeral agents - // Real agent IDs always start with "agent_", so anything else is ephemeral + const afterPrimaryCheck = () => addedConvoMiddleware(req, res, next); + if (isEphemeralAgentId(agentId)) { - return next(); + return afterPrimaryCheck(); } - const agentAccessMiddleware = canAccessResource({ - resourceType: ResourceType.AGENT, - requiredPermission, - resourceIdParam: 'agent_id', // This will be ignored since we use custom resolver - idResolver: () => resolveAgentIdFromBody(agentId), - }); - - const tempReq = { - ...req, - params: { - ...req.params, - agent_id: agentId, - }, - }; - - return agentAccessMiddleware(tempReq, res, next); + return checkAgentResourceAccess(agentId, requiredPermission, req, res, afterPrimaryCheck); } catch (error) { logger.error('Failed to validate agent access permissions', error); return res.status(500).json({ diff --git a/api/server/middleware/accessResources/canAccessAgentFromBody.spec.js b/api/server/middleware/accessResources/canAccessAgentFromBody.spec.js new file mode 100644 index 0000000000..47f1130d13 --- /dev/null +++ b/api/server/middleware/accessResources/canAccessAgentFromBody.spec.js @@ -0,0 +1,509 @@ +const mongoose = require('mongoose'); +const { + ResourceType, + SystemRoles, + PrincipalType, + PrincipalModel, +} = require('librechat-data-provider'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { canAccessAgentFromBody } = require('./canAccessAgentFromBody'); +const { User, Role, AclEntry } = require('~/db/models'); +const { createAgent } = require('~/models/Agent'); + +describe('canAccessAgentFromBody middleware', () => { + let mongoServer; + let req, res, next; + let testUser, otherUser; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await mongoose.connection.dropDatabase(); + + await Role.create({ + name: 'test-role', + permissions: { + AGENTS: { USE: true, CREATE: true, SHARE: true }, + MULTI_CONVO: { USE: true }, + }, + }); + + await Role.create({ + name: 'no-multi-convo', + permissions: { + AGENTS: { USE: true, CREATE: true, SHARE: true }, + MULTI_CONVO: { USE: false }, + }, + }); + + await Role.create({ + name: SystemRoles.ADMIN, + permissions: { + AGENTS: { USE: true, CREATE: true, SHARE: true }, + MULTI_CONVO: { USE: true }, + }, + }); + + testUser = await User.create({ + email: 'test@example.com', + name: 'Test User', + username: 'testuser', + role: 'test-role', + }); + + otherUser = await User.create({ + email: 'other@example.com', + name: 'Other User', + username: 'otheruser', + role: 'test-role', + }); + + req = { + user: { id: testUser._id, role: testUser.role }, + params: {}, + body: { + endpoint: 'agents', + agent_id: 'ephemeral_primary', + }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + next = jest.fn(); + + jest.clearAllMocks(); + }); + + describe('middleware factory', () => { + test('throws if requiredPermission is missing', () => { + expect(() => canAccessAgentFromBody({})).toThrow( + 'canAccessAgentFromBody: requiredPermission is required and must be a number', + ); + }); + + test('throws if requiredPermission is not a number', () => { + expect(() => canAccessAgentFromBody({ requiredPermission: '1' })).toThrow( + 'canAccessAgentFromBody: requiredPermission is required and must be a number', + ); + }); + + test('returns a middleware function', () => { + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + expect(typeof middleware).toBe('function'); + expect(middleware.length).toBe(3); + }); + }); + + describe('primary agent checks', () => { + test('returns 400 when agent_id is missing on agents endpoint', async () => { + req.body.agent_id = undefined; + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(400); + }); + + test('proceeds for ephemeral primary agent without addedConvo', async () => { + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + test('proceeds for non-agents endpoint (ephemeral fallback)', async () => { + req.body.endpoint = 'openAI'; + req.body.agent_id = undefined; + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + }); + + describe('addedConvo — absent or invalid shape', () => { + test('calls next when addedConvo is absent', async () => { + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('calls next when addedConvo is a string', async () => { + req.body.addedConvo = 'not-an-object'; + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('calls next when addedConvo is an array', async () => { + req.body.addedConvo = [{ agent_id: 'agent_something' }]; + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + }); + + describe('addedConvo — MULTI_CONVO permission gate', () => { + test('returns 403 when user lacks MULTI_CONVO:USE', async () => { + req.user.role = 'no-multi-convo'; + req.body.addedConvo = { agent_id: 'agent_x', endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Multi-conversation feature is not enabled' }), + ); + }); + + test('returns 403 when user.role is missing', async () => { + req.user = { id: testUser._id }; + req.body.addedConvo = { agent_id: 'agent_x', endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + + test('ADMIN bypasses MULTI_CONVO check', async () => { + req.user.role = SystemRoles.ADMIN; + req.body.addedConvo = { agent_id: 'ephemeral_x', endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + }); + + describe('addedConvo — agent_id shape validation', () => { + test('calls next when agent_id is ephemeral', async () => { + req.body.addedConvo = { agent_id: 'ephemeral_xyz', endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('calls next when agent_id is absent', async () => { + req.body.addedConvo = { endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('calls next when agent_id is not a string (object injection)', async () => { + req.body.addedConvo = { agent_id: { $gt: '' }, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + }); + + describe('addedConvo — agent resource ACL (IDOR prevention)', () => { + let addedAgent; + + beforeEach(async () => { + addedAgent = await createAgent({ + id: `agent_added_${Date.now()}`, + name: 'Private Agent', + provider: 'openai', + model: 'gpt-4', + author: otherUser._id, + }); + + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: otherUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 15, + grantedBy: otherUser._id, + }); + }); + + test('returns 403 when requester has no ACL for the added agent', async () => { + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Insufficient permissions to access this agent', + }), + ); + }); + + test('returns 404 when added agent does not exist', async () => { + req.body.addedConvo = { + agent_id: 'agent_nonexistent_999', + endpoint: 'agents', + model: 'gpt-4', + }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(404); + }); + + test('proceeds when requester has ACL for the added agent', async () => { + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: testUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 1, + grantedBy: otherUser._id, + }); + + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + test('denies when ACL permission bits are insufficient', async () => { + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: testUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 1, + grantedBy: otherUser._id, + }); + + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 2 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + + test('caches resolved agent on req.resolvedAddedAgent', async () => { + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: testUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 1, + grantedBy: otherUser._id, + }); + + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(req.resolvedAddedAgent).toBeDefined(); + expect(req.resolvedAddedAgent._id.toString()).toBe(addedAgent._id.toString()); + }); + + test('ADMIN bypasses agent resource ACL for addedConvo', async () => { + req.user.role = SystemRoles.ADMIN; + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + expect(req.resolvedAddedAgent).toBeUndefined(); + }); + }); + + describe('end-to-end: primary real agent + addedConvo real agent', () => { + let primaryAgent, addedAgent; + + beforeEach(async () => { + primaryAgent = await createAgent({ + id: `agent_primary_${Date.now()}`, + name: 'Primary Agent', + provider: 'openai', + model: 'gpt-4', + author: testUser._id, + }); + + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: testUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: primaryAgent._id, + permBits: 15, + grantedBy: testUser._id, + }); + + addedAgent = await createAgent({ + id: `agent_added_${Date.now()}`, + name: 'Added Agent', + provider: 'openai', + model: 'gpt-4', + author: otherUser._id, + }); + + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: otherUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 15, + grantedBy: otherUser._id, + }); + + req.body.agent_id = primaryAgent.id; + }); + + test('both checks pass when user has ACL for both agents', async () => { + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: testUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 1, + grantedBy: otherUser._id, + }); + + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + expect(req.resolvedAddedAgent).toBeDefined(); + }); + + test('primary passes but addedConvo denied → 403', async () => { + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + + test('primary denied → 403 without reaching addedConvo check', async () => { + const foreignAgent = await createAgent({ + id: `agent_foreign_${Date.now()}`, + name: 'Foreign Agent', + provider: 'openai', + model: 'gpt-4', + author: otherUser._id, + }); + + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: otherUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: foreignAgent._id, + permBits: 15, + grantedBy: otherUser._id, + }); + + req.body.agent_id = foreignAgent.id; + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + }); + + describe('ephemeral primary + real addedConvo agent', () => { + let addedAgent; + + beforeEach(async () => { + addedAgent = await createAgent({ + id: `agent_added_${Date.now()}`, + name: 'Added Agent', + provider: 'openai', + model: 'gpt-4', + author: otherUser._id, + }); + + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: otherUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 15, + grantedBy: otherUser._id, + }); + }); + + test('runs full addedConvo ACL check even when primary is ephemeral', async () => { + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + + test('proceeds when user has ACL for added agent (ephemeral primary)', async () => { + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: testUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: addedAgent._id, + permBits: 1, + grantedBy: otherUser._id, + }); + + req.body.addedConvo = { agent_id: addedAgent.id, endpoint: 'agents', model: 'gpt-4' }; + + const middleware = canAccessAgentFromBody({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + }); +}); From 1312cd757c3e2634d59a73fb96f30dcd4c0d17e3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 18:05:08 -0400 Subject: [PATCH 042/111] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20Validate?= =?UTF-8?q?=20User-provided=20URLs=20for=20Web=20Search=20(#12247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: SSRF-validate user-provided URLs in web search auth User-controlled URL fields (jinaApiUrl, firecrawlApiUrl, searxngInstanceUrl) flow from plugin auth into outbound HTTP requests without validation. Reuse existing isSSRFTarget/resolveHostnameSSRF to block private/internal targets while preserving admin-configured (env var) internal URLs. * 🛡️ fix: Harden web search SSRF validation - Reject non-HTTP(S) schemes (file://, ftp://, etc.) in isSSRFUrl - Conditional write: only assign to authResult after SSRF check passes - Move isUserProvided tracking after SSRF gate to avoid false positives - Add authenticated assertions for optional-field SSRF blocks in tests - Add file:// scheme rejection test - Wrap process.env mutation in try/finally guard - Add JSDoc + sync-obligation comment on WEB_SEARCH_URL_KEYS * 🛡️ fix: Correct auth-type reporting for SSRF-stripped optional URLs SSRF-stripped optional URL fields no longer pollute isUserProvided. Track whether the field actually contributed to authResult before crediting it as user-provided, so categories report SYSTEM_DEFINED when all surviving values match env vars. --- packages/api/src/web/web.spec.ts | 360 +++++++++++++++++++++++++++++++ packages/api/src/web/web.ts | 50 ++++- 2 files changed, 408 insertions(+), 2 deletions(-) diff --git a/packages/api/src/web/web.spec.ts b/packages/api/src/web/web.spec.ts index c7bb3f4962..74e02b20ef 100644 --- a/packages/api/src/web/web.spec.ts +++ b/packages/api/src/web/web.spec.ts @@ -18,6 +18,14 @@ jest.mock('../utils', () => ({ }, })); +const mockIsSSRFTarget = jest.fn().mockReturnValue(false); +const mockResolveHostnameSSRF = jest.fn().mockResolvedValue(false); + +jest.mock('../auth', () => ({ + isSSRFTarget: (...args: unknown[]) => mockIsSSRFTarget(...args), + resolveHostnameSSRF: (...args: unknown[]) => mockResolveHostnameSSRF(...args), +})); + describe('web.ts', () => { describe('extractWebSearchEnvVars', () => { it('should return empty array if config is undefined', () => { @@ -1227,4 +1235,356 @@ describe('web.ts', () => { expect(result.authResult.firecrawlOptions).toBeUndefined(); // Should be undefined }); }); + + describe('SSRF protection for user-provided URLs', () => { + const userId = 'test-user-id'; + let mockLoadAuthValues: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockLoadAuthValues = jest.fn(); + mockIsSSRFTarget.mockReturnValue(false); + mockResolveHostnameSSRF.mockResolvedValue(false); + }); + + it('should block user-provided jinaApiUrl targeting localhost', async () => { + mockIsSSRFTarget.mockImplementation((hostname: string) => hostname === 'localhost'); + + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + jinaApiKey: '${JINA_API_KEY}', + jinaApiUrl: '${JINA_API_URL}', + safeSearch: SafeSearchTypes.MODERATE, + rerankerType: 'jina' as RerankerTypes, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'JINA_API_URL') { + result[field] = 'http://localhost:8080/rerank'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.jinaApiUrl).toBeUndefined(); + expect(mockIsSSRFTarget).toHaveBeenCalledWith('localhost'); + }); + + it('should block user-provided firecrawlApiUrl resolving to private IP', async () => { + mockResolveHostnameSSRF.mockImplementation((hostname: string) => + Promise.resolve(hostname === 'evil.internal-service.com'), + ); + + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + safeSearch: SafeSearchTypes.MODERATE, + scraperProvider: 'firecrawl' as ScraperProviders, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'FIRECRAWL_API_URL') { + result[field] = 'https://evil.internal-service.com/scrape'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.firecrawlApiUrl).toBeUndefined(); + expect(result.authenticated).toBe(true); + const scrapersAuth = result.authTypes.find(([c]) => c === 'scrapers')?.[1]; + expect(scrapersAuth).toBe(AuthType.USER_PROVIDED); + }); + + it('should block user-provided searxngInstanceUrl targeting metadata endpoint', async () => { + mockIsSSRFTarget.mockImplementation((hostname: string) => hostname === '169.254.169.254'); + + const webSearchConfig: TCustomConfig['webSearch'] = { + searxngInstanceUrl: '${SEARXNG_INSTANCE_URL}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + jinaApiKey: '${JINA_API_KEY}', + safeSearch: SafeSearchTypes.MODERATE, + searchProvider: 'searxng' as SearchProviders, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'SEARXNG_INSTANCE_URL') { + result[field] = 'http://169.254.169.254/latest/meta-data'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.searxngInstanceUrl).toBeUndefined(); + expect(result.authenticated).toBe(false); + }); + + it('should allow system-defined URLs even if they match SSRF patterns', async () => { + mockIsSSRFTarget.mockReturnValue(true); + + const originalEnv = process.env; + try { + process.env = { + ...originalEnv, + JINA_API_KEY: 'system-jina-key', + JINA_API_URL: 'http://jina-internal:8080/rerank', + }; + + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + jinaApiKey: '${JINA_API_KEY}', + jinaApiUrl: '${JINA_API_URL}', + safeSearch: SafeSearchTypes.MODERATE, + rerankerType: 'jina' as RerankerTypes, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'JINA_API_KEY') { + result[field] = 'system-jina-key'; + } else if (field === 'JINA_API_URL') { + result[field] = 'http://jina-internal:8080/rerank'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.jinaApiUrl).toBe('http://jina-internal:8080/rerank'); + expect(result.authenticated).toBe(true); + } finally { + process.env = originalEnv; + } + }); + + it('should reject URLs with invalid format', async () => { + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + safeSearch: SafeSearchTypes.MODERATE, + scraperProvider: 'firecrawl' as ScraperProviders, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'FIRECRAWL_API_URL') { + result[field] = 'not-a-valid-url'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.firecrawlApiUrl).toBeUndefined(); + expect(result.authenticated).toBe(true); + const scrapersAuth = result.authTypes.find(([c]) => c === 'scrapers')?.[1]; + expect(scrapersAuth).toBe(AuthType.USER_PROVIDED); + }); + + it('should reject non-HTTP schemes like file://', async () => { + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + safeSearch: SafeSearchTypes.MODERATE, + scraperProvider: 'firecrawl' as ScraperProviders, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'FIRECRAWL_API_URL') { + result[field] = 'file:///etc/passwd'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.firecrawlApiUrl).toBeUndefined(); + expect(result.authenticated).toBe(true); + }); + + it('should allow legitimate external URLs', async () => { + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + jinaApiUrl: '${JINA_API_URL}', + safeSearch: SafeSearchTypes.MODERATE, + scraperProvider: 'firecrawl' as ScraperProviders, + rerankerType: 'jina' as RerankerTypes, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'FIRECRAWL_API_URL') { + result[field] = 'https://api.firecrawl.dev'; + } else if (field === 'JINA_API_URL') { + result[field] = 'https://api.jina.ai/v1/rerank'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.firecrawlApiUrl).toBe('https://api.firecrawl.dev'); + expect(result.authResult.jinaApiUrl).toBe('https://api.jina.ai/v1/rerank'); + expect(result.authenticated).toBe(true); + }); + + it('should fail required URL field and mark category unauthenticated', async () => { + mockIsSSRFTarget.mockImplementation((hostname: string) => hostname === '127.0.0.1'); + + const webSearchConfig: TCustomConfig['webSearch'] = { + searxngInstanceUrl: '${SEARXNG_INSTANCE_URL}', + searxngApiKey: '${SEARXNG_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + jinaApiKey: '${JINA_API_KEY}', + safeSearch: SafeSearchTypes.MODERATE, + searchProvider: 'searxng' as SearchProviders, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'SEARXNG_INSTANCE_URL') { + result[field] = 'http://127.0.0.1:8888/search'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(false); + const providersAuthType = result.authTypes.find( + ([category]) => category === 'providers', + )?.[1]; + expect(providersAuthType).toBe(AuthType.USER_PROVIDED); + }); + + it('should report SYSTEM_DEFINED when only user-provided field is a stripped SSRF URL', async () => { + mockIsSSRFTarget.mockImplementation((hostname: string) => hostname === 'localhost'); + + const originalEnv = process.env; + try { + process.env = { + ...originalEnv, + JINA_API_KEY: 'system-jina-key', + }; + + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + jinaApiKey: '${JINA_API_KEY}', + jinaApiUrl: '${JINA_API_URL}', + safeSearch: SafeSearchTypes.MODERATE, + rerankerType: 'jina' as RerankerTypes, + }; + + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field: string) => { + if (field === 'JINA_API_KEY') { + result[field] = 'system-jina-key'; + } else if (field === 'JINA_API_URL') { + result[field] = 'http://localhost:9999/rerank'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult.jinaApiUrl).toBeUndefined(); + expect(result.authenticated).toBe(true); + const rerankersAuth = result.authTypes.find(([c]) => c === 'rerankers')?.[1]; + expect(rerankersAuth).toBe(AuthType.SYSTEM_DEFINED); + } finally { + process.env = originalEnv; + } + }); + }); }); diff --git a/packages/api/src/web/web.ts b/packages/api/src/web/web.ts index ad172e187f..cc0d8688ca 100644 --- a/packages/api/src/web/web.ts +++ b/packages/api/src/web/web.ts @@ -13,6 +13,37 @@ import type { TWebSearchConfig, } from 'librechat-data-provider'; import type { TWebSearchKeys, TWebSearchCategories } from '@librechat/data-schemas'; +import { isSSRFTarget, resolveHostnameSSRF } from '../auth'; + +/** + * URL-type keys in TWebSearchKeys (not API keys or version strings). + * Must stay in sync with URL-typed fields in webSearchAuth (packages/data-schemas). + */ +const WEB_SEARCH_URL_KEYS = new Set([ + 'searxngInstanceUrl', + 'firecrawlApiUrl', + 'jinaApiUrl', +]); + +/** + * Returns true if the URL should be blocked for SSRF risk. + * Fail-closed: unparseable URLs and non-HTTP(S) schemes return true. + */ +async function isSSRFUrl(url: string): Promise { + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return true; + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return true; + } + if (isSSRFTarget(parsed.hostname)) { + return true; + } + return resolveHostnameSSRF(parsed.hostname); +} export function extractWebSearchEnvVars({ keys, @@ -149,12 +180,27 @@ export async function loadWebSearchAuth({ const field = allAuthFields[j]; const value = authValues[field]; const originalKey = allKeys[j]; - if (originalKey) authResult[originalKey] = value; + if (!optionalSet.has(field) && !value) { allFieldsAuthenticated = false; break; } - if (!isUserProvided && process.env[field] !== value) { + + const isFieldUserProvided = value != null && process.env[field] !== value; + const isUrlKey = originalKey != null && WEB_SEARCH_URL_KEYS.has(originalKey); + let contributed = false; + + if (isUrlKey && isFieldUserProvided && (await isSSRFUrl(value))) { + if (!optionalSet.has(field)) { + allFieldsAuthenticated = false; + break; + } + } else if (originalKey) { + authResult[originalKey] = value; + contributed = true; + } + + if (!isUserProvided && isFieldUserProvided && contributed) { isUserProvided = true; } } From bcf45519bd5c10740ec38cf37f74ee7d1bac287a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 18:08:57 -0400 Subject: [PATCH 043/111] =?UTF-8?q?=F0=9F=AA=AA=20fix:=20Enforce=20VIEW=20?= =?UTF-8?q?ACL=20on=20Agent=20Edge=20References=20at=20Write=20and=20Runti?= =?UTF-8?q?me=20(#12246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Enforce ACL checks on handoff edge and added-convo agent loading Edge-linked agents and added-convo agents were fetched by ID via getAgent without verifying the requesting user's access permissions. This allowed an authenticated user to reference another user's private agent in edges or addedConvo and have it initialized at runtime. Add checkPermission(VIEW) gate in processAgent before initializing any handoff agent, and in processAddedConvo for non-ephemeral added agents. Unauthorized agents are logged and added to skippedAgentIds so orphaned-edge filtering removes them cleanly. * 🛡️ fix: Validate edge agent access at agent create/update time Reject agent create/update requests that reference agents in edges the requesting user cannot VIEW. This provides early feedback and prevents storing unauthorized agent references as defense-in-depth alongside the runtime ACL gate in processAgent. Add collectEdgeAgentIds utility to extract all unique agent IDs from an edge array, and validateEdgeAgentAccess helper in the v1 handler. * 🧪 test: Improve ACL gate test coverage and correctness - Add processAgent ACL gate tests for initializeClient (skip/allow handoff agents) - Fix addedConvo.spec.js to mock loadAddedAgent directly instead of getAgent - Seed permMap with ownedAgent VIEW bits in v1.spec.js update-403 test * 🧹 chore: Remove redundant addedConvo ACL gate (now in middleware) PR #12243 moved the addedConvo agent ACL check upstream into canAccessAgentFromBody middleware, making the runtime check in processAddedConvo and its spec redundant. * 🧪 test: Rewrite processAgent ACL test with real DB and minimal mocking Replace heavy mock-based test (12 mocks, Providers.XAI crash) with MongoMemoryServer-backed integration test that exercises real getAgent, checkPermission, and AclEntry — only external I/O (initializeAgent, ToolService, AgentClient) remains mocked. Load edge utilities directly from packages/api/src/agents/edges to sidestep the config.ts barrel. * 🧪 fix: Use requireActual spread for @librechat/agents and @librechat/api mocks The Providers.XAI crash was caused by mocking @librechat/agents with a minimal replacement object, breaking the @librechat/api initialization chain. Match the established pattern from client.test.js and recordCollectedUsage.spec.js: spread jest.requireActual for both packages, overriding only the functions under test. --- api/server/controllers/agents/v1.js | 63 +++++- api/server/controllers/agents/v1.spec.js | 113 +++++++++- .../services/Endpoints/agents/initialize.js | 19 ++ .../Endpoints/agents/initialize.spec.js | 201 ++++++++++++++++++ packages/api/src/agents/edges.spec.ts | 51 ++++- packages/api/src/agents/edges.ts | 14 ++ 6 files changed, 457 insertions(+), 4 deletions(-) create mode 100644 api/server/services/Endpoints/agents/initialize.spec.js diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 1abba8b2c8..dbb97df24b 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -6,6 +6,7 @@ const { agentCreateSchema, agentUpdateSchema, refreshListAvatars, + collectEdgeAgentIds, mergeAgentOcrConversion, MAX_AVATAR_REFRESH_AGENTS, convertOcrToContextInPlace, @@ -35,6 +36,7 @@ const { } = require('~/models/Agent'); const { findPubliclyAccessibleResources, + getResourcePermissionsMap, findAccessibleResources, hasPublicPermission, grantPermission, @@ -58,6 +60,44 @@ const systemTools = { const MAX_SEARCH_LEN = 100; const escapeRegex = (str = '') => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +/** + * Validates that the requesting user has VIEW access to every agent referenced in edges. + * Agents that do not exist in the database are skipped — at create time, the `from` field + * often references the agent being built, which has no DB record yet. + * @param {import('librechat-data-provider').GraphEdge[]} edges + * @param {string} userId + * @param {string} userRole - Used for group/role principal resolution + * @returns {Promise} Agent IDs the user cannot VIEW (empty if all accessible) + */ +const validateEdgeAgentAccess = async (edges, userId, userRole) => { + const edgeAgentIds = collectEdgeAgentIds(edges); + if (edgeAgentIds.size === 0) { + return []; + } + + const agents = (await Promise.all([...edgeAgentIds].map((id) => getAgent({ id })))).filter( + Boolean, + ); + + if (agents.length === 0) { + return []; + } + + const permissionsMap = await getResourcePermissionsMap({ + userId, + role: userRole, + resourceType: ResourceType.AGENT, + resourceIds: agents.map((a) => a._id), + }); + + return agents + .filter((a) => { + const bits = permissionsMap.get(a._id.toString()) ?? 0; + return (bits & PermissionBits.VIEW) === 0; + }) + .map((a) => a.id); +}; + /** * Creates an Agent. * @route POST /Agents @@ -75,7 +115,17 @@ const createAgentHandler = async (req, res) => { agentData.model_parameters = removeNullishValues(agentData.model_parameters, true); } - const { id: userId } = req.user; + const { id: userId, role: userRole } = req.user; + + if (agentData.edges?.length) { + const unauthorized = await validateEdgeAgentAccess(agentData.edges, userId, userRole); + if (unauthorized.length > 0) { + return res.status(403).json({ + error: 'You do not have access to one or more agents referenced in edges', + agent_ids: unauthorized, + }); + } + } agentData.id = `agent_${nanoid()}`; agentData.author = userId; @@ -243,6 +293,17 @@ const updateAgentHandler = async (req, res) => { updateData.avatar = avatarField; } + if (updateData.edges?.length) { + const { id: userId, role: userRole } = req.user; + const unauthorized = await validateEdgeAgentAccess(updateData.edges, userId, userRole); + if (unauthorized.length > 0) { + return res.status(403).json({ + error: 'You do not have access to one or more agents referenced in edges', + agent_ids: unauthorized, + }); + } + } + // Convert OCR to context in incoming updateData convertOcrToContextInPlace(updateData); diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index ce68cc241f..ede4ea416a 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const { nanoid } = require('nanoid'); const { v4: uuidv4 } = require('uuid'); const { agentSchema } = require('@librechat/data-schemas'); -const { FileSources } = require('librechat-data-provider'); +const { FileSources, PermissionBits } = require('librechat-data-provider'); const { MongoMemoryServer } = require('mongodb-memory-server'); // Only mock the dependencies that are not database-related @@ -46,9 +46,9 @@ jest.mock('~/models/File', () => ({ jest.mock('~/server/services/PermissionService', () => ({ findAccessibleResources: jest.fn().mockResolvedValue([]), findPubliclyAccessibleResources: jest.fn().mockResolvedValue([]), + getResourcePermissionsMap: jest.fn().mockResolvedValue(new Map()), grantPermission: jest.fn(), hasPublicPermission: jest.fn().mockResolvedValue(false), - checkPermission: jest.fn().mockResolvedValue(true), })); jest.mock('~/models', () => ({ @@ -74,6 +74,7 @@ const { const { findAccessibleResources, findPubliclyAccessibleResources, + getResourcePermissionsMap, } = require('~/server/services/PermissionService'); const { refreshS3Url } = require('~/server/services/Files/S3/crud'); @@ -1647,4 +1648,112 @@ describe('Agent Controllers - Mass Assignment Protection', () => { expect(agent.avatar.filepath).toBe('old-s3-path.jpg'); }); }); + + describe('Edge ACL validation', () => { + let targetAgent; + + beforeEach(async () => { + targetAgent = await Agent.create({ + id: `agent_${nanoid()}`, + author: new mongoose.Types.ObjectId().toString(), + name: 'Target Agent', + provider: 'openai', + model: 'gpt-4', + tools: [], + }); + }); + + test('createAgentHandler should return 403 when user lacks VIEW on an edge-referenced agent', async () => { + const permMap = new Map(); + getResourcePermissionsMap.mockResolvedValueOnce(permMap); + + mockReq.body = { + name: 'Attacker Agent', + provider: 'openai', + model: 'gpt-4', + edges: [{ from: 'self_placeholder', to: targetAgent.id, edgeType: 'handoff' }], + }; + + await createAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(403); + const response = mockRes.json.mock.calls[0][0]; + expect(response.agent_ids).toContain(targetAgent.id); + }); + + test('createAgentHandler should succeed when user has VIEW on all edge-referenced agents', async () => { + const permMap = new Map([[targetAgent._id.toString(), 1]]); + getResourcePermissionsMap.mockResolvedValueOnce(permMap); + + mockReq.body = { + name: 'Legit Agent', + provider: 'openai', + model: 'gpt-4', + edges: [{ from: 'self_placeholder', to: targetAgent.id, edgeType: 'handoff' }], + }; + + await createAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(201); + }); + + test('createAgentHandler should allow edges referencing non-existent agents (self-reference at create time)', async () => { + mockReq.body = { + name: 'Self-Ref Agent', + provider: 'openai', + model: 'gpt-4', + edges: [{ from: 'agent_does_not_exist_yet', to: 'agent_also_new', edgeType: 'handoff' }], + }; + + await createAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(201); + }); + + test('updateAgentHandler should return 403 when user lacks VIEW on an edge-referenced agent', async () => { + const ownedAgent = await Agent.create({ + id: `agent_${nanoid()}`, + author: mockReq.user.id, + name: 'Owned Agent', + provider: 'openai', + model: 'gpt-4', + tools: [], + }); + + const permMap = new Map([[ownedAgent._id.toString(), PermissionBits.VIEW]]); + getResourcePermissionsMap.mockResolvedValueOnce(permMap); + + mockReq.params = { id: ownedAgent.id }; + mockReq.body = { + edges: [{ from: ownedAgent.id, to: targetAgent.id, edgeType: 'handoff' }], + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(403); + const response = mockRes.json.mock.calls[0][0]; + expect(response.agent_ids).toContain(targetAgent.id); + expect(response.agent_ids).not.toContain(ownedAgent.id); + }); + + test('updateAgentHandler should succeed when edges field is absent from payload', async () => { + const ownedAgent = await Agent.create({ + id: `agent_${nanoid()}`, + author: mockReq.user.id, + name: 'Owned Agent', + provider: 'openai', + model: 'gpt-4', + tools: [], + }); + + mockReq.params = { id: ownedAgent.id }; + mockReq.body = { name: 'Renamed Agent' }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.status).not.toHaveBeenCalledWith(403); + const response = mockRes.json.mock.calls[0][0]; + expect(response.name).toBe('Renamed Agent'); + }); + }); }); diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index e71270ef85..44583e6dbc 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -10,6 +10,8 @@ const { createSequentialChainEdges, } = require('@librechat/api'); const { + ResourceType, + PermissionBits, EModelEndpoint, isAgentsEndpoint, getResponseSender, @@ -21,6 +23,7 @@ const { } = require('~/server/controllers/agents/callbacks'); const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); const { getModelsConfig } = require('~/server/controllers/ModelController'); +const { checkPermission } = require('~/server/services/PermissionService'); const AgentClient = require('~/server/controllers/agents/client'); const { getConvoFiles } = require('~/models/Conversation'); const { processAddedConvo } = require('./addedConvo'); @@ -229,6 +232,22 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { return null; } + const hasAccess = await checkPermission({ + userId: req.user.id, + role: req.user.role, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + requiredPermission: PermissionBits.VIEW, + }); + + if (!hasAccess) { + logger.warn( + `[processAgent] User ${req.user.id} lacks VIEW access to handoff agent ${agentId}, skipping`, + ); + skippedAgentIds.add(agentId); + return null; + } + const validationResult = await validateAgentModel({ req, res, diff --git a/api/server/services/Endpoints/agents/initialize.spec.js b/api/server/services/Endpoints/agents/initialize.spec.js new file mode 100644 index 0000000000..16b41aca65 --- /dev/null +++ b/api/server/services/Endpoints/agents/initialize.spec.js @@ -0,0 +1,201 @@ +const mongoose = require('mongoose'); +const { + ResourceType, + PermissionBits, + PrincipalType, + PrincipalModel, +} = require('librechat-data-provider'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +const mockInitializeAgent = jest.fn(); +const mockValidateAgentModel = jest.fn(); + +jest.mock('@librechat/agents', () => ({ + ...jest.requireActual('@librechat/agents'), + createContentAggregator: jest.fn(() => ({ + contentParts: [], + aggregateContent: jest.fn(), + })), +})); + +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), + initializeAgent: (...args) => mockInitializeAgent(...args), + validateAgentModel: (...args) => mockValidateAgentModel(...args), + GenerationJobManager: { setCollectedUsage: jest.fn() }, + getCustomEndpointConfig: jest.fn(), + createSequentialChainEdges: jest.fn(), +})); + +jest.mock('~/server/controllers/agents/callbacks', () => ({ + createToolEndCallback: jest.fn(() => jest.fn()), + getDefaultHandlers: jest.fn(() => ({})), +})); + +jest.mock('~/server/services/ToolService', () => ({ + loadAgentTools: jest.fn(), + loadToolsForExecution: jest.fn(), +})); + +jest.mock('~/server/controllers/ModelController', () => ({ + getModelsConfig: jest.fn().mockResolvedValue({}), +})); + +let agentClientArgs; +jest.mock('~/server/controllers/agents/client', () => { + return jest.fn().mockImplementation((args) => { + agentClientArgs = args; + return {}; + }); +}); + +jest.mock('./addedConvo', () => ({ + processAddedConvo: jest.fn().mockResolvedValue({ userMCPAuthMap: undefined }), +})); + +jest.mock('~/cache', () => ({ + logViolation: jest.fn(), +})); + +const { initializeClient } = require('./initialize'); +const { createAgent } = require('~/models/Agent'); +const { User, AclEntry } = require('~/db/models'); + +const PRIMARY_ID = 'agent_primary'; +const TARGET_ID = 'agent_target'; +const AUTHORIZED_ID = 'agent_authorized'; + +describe('initializeClient — processAgent ACL gate', () => { + let mongoServer; + let testUser; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await mongoose.connection.dropDatabase(); + jest.clearAllMocks(); + agentClientArgs = undefined; + + testUser = await User.create({ + email: 'test@example.com', + name: 'Test User', + username: 'testuser', + role: 'USER', + }); + + mockValidateAgentModel.mockResolvedValue({ isValid: true }); + }); + + const makeReq = () => ({ + user: { id: testUser._id.toString(), role: 'USER' }, + body: { conversationId: 'conv_1', files: [] }, + config: { endpoints: {} }, + _resumableStreamId: null, + }); + + const makeEndpointOption = () => ({ + agent: Promise.resolve({ + id: PRIMARY_ID, + name: 'Primary', + provider: 'openai', + model: 'gpt-4', + tools: [], + }), + model_parameters: { model: 'gpt-4' }, + endpoint: 'agents', + }); + + const makePrimaryConfig = (edges) => ({ + id: PRIMARY_ID, + endpoint: 'agents', + edges, + toolDefinitions: [], + toolRegistry: new Map(), + userMCPAuthMap: null, + tool_resources: {}, + resendFiles: true, + maxContextTokens: 4096, + }); + + it('should skip handoff agent and filter its edge when user lacks VIEW access', async () => { + await createAgent({ + id: TARGET_ID, + name: 'Target Agent', + provider: 'openai', + model: 'gpt-4', + author: new mongoose.Types.ObjectId(), + tools: [], + }); + + const edges = [{ from: PRIMARY_ID, to: TARGET_ID, edgeType: 'handoff' }]; + mockInitializeAgent.mockResolvedValue(makePrimaryConfig(edges)); + + await initializeClient({ + req: makeReq(), + res: {}, + signal: new AbortController().signal, + endpointOption: makeEndpointOption(), + }); + + expect(mockInitializeAgent).toHaveBeenCalledTimes(1); + expect(agentClientArgs.agent.edges).toEqual([]); + }); + + it('should initialize handoff agent and keep its edge when user has VIEW access', async () => { + const authorizedAgent = await createAgent({ + id: AUTHORIZED_ID, + name: 'Authorized Agent', + provider: 'openai', + model: 'gpt-4', + author: new mongoose.Types.ObjectId(), + tools: [], + }); + + await AclEntry.create({ + principalType: PrincipalType.USER, + principalId: testUser._id, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: authorizedAgent._id, + permBits: PermissionBits.VIEW, + grantedBy: testUser._id, + }); + + const edges = [{ from: PRIMARY_ID, to: AUTHORIZED_ID, edgeType: 'handoff' }]; + const handoffConfig = { + id: AUTHORIZED_ID, + edges: [], + toolDefinitions: [], + toolRegistry: new Map(), + userMCPAuthMap: null, + tool_resources: {}, + }; + + let callCount = 0; + mockInitializeAgent.mockImplementation(() => { + callCount++; + return callCount === 1 + ? Promise.resolve(makePrimaryConfig(edges)) + : Promise.resolve(handoffConfig); + }); + + await initializeClient({ + req: makeReq(), + res: {}, + signal: new AbortController().signal, + endpointOption: makeEndpointOption(), + }); + + expect(mockInitializeAgent).toHaveBeenCalledTimes(2); + expect(agentClientArgs.agent.edges).toHaveLength(1); + expect(agentClientArgs.agent.edges[0].to).toBe(AUTHORIZED_ID); + }); +}); diff --git a/packages/api/src/agents/edges.spec.ts b/packages/api/src/agents/edges.spec.ts index 1b30a202d0..b23f00f63f 100644 --- a/packages/api/src/agents/edges.spec.ts +++ b/packages/api/src/agents/edges.spec.ts @@ -1,5 +1,11 @@ import type { GraphEdge } from 'librechat-data-provider'; -import { getEdgeKey, getEdgeParticipants, filterOrphanedEdges, createEdgeCollector } from './edges'; +import { + getEdgeKey, + getEdgeParticipants, + collectEdgeAgentIds, + filterOrphanedEdges, + createEdgeCollector, +} from './edges'; describe('edges utilities', () => { describe('getEdgeKey', () => { @@ -70,6 +76,49 @@ describe('edges utilities', () => { }); }); + describe('collectEdgeAgentIds', () => { + it('should return empty set for undefined input', () => { + expect(collectEdgeAgentIds(undefined)).toEqual(new Set()); + }); + + it('should return empty set for empty array', () => { + expect(collectEdgeAgentIds([])).toEqual(new Set()); + }); + + it('should collect IDs from simple string from/to', () => { + const edges: GraphEdge[] = [{ from: 'agent_a', to: 'agent_b', edgeType: 'handoff' }]; + expect(collectEdgeAgentIds(edges)).toEqual(new Set(['agent_a', 'agent_b'])); + }); + + it('should collect IDs from array from/to values', () => { + const edges: GraphEdge[] = [ + { from: ['agent_a', 'agent_b'], to: ['agent_c', 'agent_d'], edgeType: 'handoff' }, + ]; + expect(collectEdgeAgentIds(edges)).toEqual( + new Set(['agent_a', 'agent_b', 'agent_c', 'agent_d']), + ); + }); + + it('should deduplicate IDs across edges', () => { + const edges: GraphEdge[] = [ + { from: 'agent_a', to: 'agent_b', edgeType: 'handoff' }, + { from: 'agent_b', to: 'agent_c', edgeType: 'handoff' }, + { from: 'agent_a', to: 'agent_c', edgeType: 'direct' }, + ]; + expect(collectEdgeAgentIds(edges)).toEqual(new Set(['agent_a', 'agent_b', 'agent_c'])); + }); + + it('should handle mixed scalar and array edges', () => { + const edges: GraphEdge[] = [ + { from: 'agent_a', to: ['agent_b', 'agent_c'], edgeType: 'handoff' }, + { from: ['agent_c', 'agent_d'], to: 'agent_e', edgeType: 'direct' }, + ]; + expect(collectEdgeAgentIds(edges)).toEqual( + new Set(['agent_a', 'agent_b', 'agent_c', 'agent_d', 'agent_e']), + ); + }); + }); + describe('filterOrphanedEdges', () => { const edges: GraphEdge[] = [ { from: 'agent_a', to: 'agent_b', edgeType: 'handoff' }, diff --git a/packages/api/src/agents/edges.ts b/packages/api/src/agents/edges.ts index 4d2883d165..9a36105b74 100644 --- a/packages/api/src/agents/edges.ts +++ b/packages/api/src/agents/edges.ts @@ -43,6 +43,20 @@ export function filterOrphanedEdges(edges: GraphEdge[], skippedAgentIds: Set { + const ids = new Set(); + if (!edges || edges.length === 0) { + return ids; + } + for (const edge of edges) { + for (const id of getEdgeParticipants(edge)) { + ids.add(id); + } + } + return ids; +} + /** * Result of discovering and aggregating edges from connected agents. */ From f9927f01687f40eb0b77b9c98308a9dc1b05898c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 18:40:42 -0400 Subject: [PATCH 044/111] =?UTF-8?q?=F0=9F=93=91=20fix:=20Sanitize=20Markdo?= =?UTF-8?q?wn=20Artifacts=20(#12249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Sanitize markdown artifact rendering to prevent stored XSS Replace marked-react with react-markdown + remark-gfm for artifact markdown preview. react-markdown's skipHtml strips raw HTML tags, and a urlTransform guard blocks javascript: and data: protocol links. * fix: Update useArtifactProps test to expect react-markdown dependencies * fix: Harden markdown artifact sanitization - Convert isSafeUrl from denylist to allowlist (http, https, mailto, tel plus relative/anchor URLs); unknown protocols are now fail-closed - Add remark-breaks to restore single-newline-to-
behavior that was silently dropped when replacing marked-react - Export isSafeUrl from the host module and add 16 direct unit tests covering allowed protocols, blocked schemes (javascript, data, blob, vbscript, file, custom), edge cases (empty, whitespace, mixed case) - Hoist remarkPlugins to a module-level constant to avoid per-render array allocation in the generated Sandpack component - Fix import order in generated template (shortest to longest per AGENTS.md) and remove pre-existing trailing whitespace * fix: Return null for blocked URLs, add sync-guard comments and test - urlTransform returns null (not '') for blocked URLs so react-markdown omits the href/src attribute entirely instead of producing - Hoist urlTransform to module-level constant alongside remarkPlugins - Add JSDoc sync-guard comments tying the exported isSafeUrl to its template-string mirror, so future maintainers know to update both - Add synchronization test asserting the embedded isSafeUrl contains the same allowlist set, URL parsing, and relative-path checks as the export --- .../__tests__/useArtifactProps.test.ts | 6 +- client/src/utils/__tests__/markdown.test.ts | 96 +++++++++++++++++-- client/src/utils/artifacts.ts | 4 +- client/src/utils/markdown.ts | 55 ++++++++++- 4 files changed, 148 insertions(+), 13 deletions(-) diff --git a/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts index f9f29e0c56..e46a285c50 100644 --- a/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts +++ b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts @@ -112,7 +112,7 @@ describe('useArtifactProps', () => { expect(result.current.files['content.md']).toBe('# No content provided'); }); - it('should provide marked-react dependency', () => { + it('should provide react-markdown dependency', () => { const artifact = createArtifact({ type: 'text/markdown', content: '# Test', @@ -120,7 +120,9 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); - expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('marked-react'); + expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('react-markdown'); + expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('remark-gfm'); + expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('remark-breaks'); }); it('should update files when content changes', () => { diff --git a/client/src/utils/__tests__/markdown.test.ts b/client/src/utils/__tests__/markdown.test.ts index fcc0f169e6..9734e0e18a 100644 --- a/client/src/utils/__tests__/markdown.test.ts +++ b/client/src/utils/__tests__/markdown.test.ts @@ -1,4 +1,72 @@ -import { getMarkdownFiles } from '../markdown'; +import { isSafeUrl, getMarkdownFiles } from '../markdown'; + +describe('isSafeUrl', () => { + it('allows https URLs', () => { + expect(isSafeUrl('https://example.com')).toBe(true); + }); + + it('allows http URLs', () => { + expect(isSafeUrl('http://example.com/path')).toBe(true); + }); + + it('allows mailto links', () => { + expect(isSafeUrl('mailto:user@example.com')).toBe(true); + }); + + it('allows tel links', () => { + expect(isSafeUrl('tel:+1234567890')).toBe(true); + }); + + it('allows relative paths', () => { + expect(isSafeUrl('/path/to/page')).toBe(true); + expect(isSafeUrl('./relative')).toBe(true); + expect(isSafeUrl('../parent')).toBe(true); + }); + + it('allows anchor links', () => { + expect(isSafeUrl('#section')).toBe(true); + }); + + it('blocks javascript: protocol', () => { + expect(isSafeUrl('javascript:alert(1)')).toBe(false); + }); + + it('blocks javascript: with leading whitespace', () => { + expect(isSafeUrl(' javascript:alert(1)')).toBe(false); + }); + + it('blocks javascript: with mixed case', () => { + expect(isSafeUrl('JavaScript:alert(1)')).toBe(false); + }); + + it('blocks data: protocol', () => { + expect(isSafeUrl('data:text/html,x')).toBe(false); + }); + + it('blocks blob: protocol', () => { + expect(isSafeUrl('blob:http://example.com/uuid')).toBe(false); + }); + + it('blocks vbscript: protocol', () => { + expect(isSafeUrl('vbscript:MsgBox("xss")')).toBe(false); + }); + + it('blocks file: protocol', () => { + expect(isSafeUrl('file:///etc/passwd')).toBe(false); + }); + + it('blocks empty strings', () => { + expect(isSafeUrl('')).toBe(false); + }); + + it('blocks whitespace-only strings', () => { + expect(isSafeUrl(' ')).toBe(false); + }); + + it('blocks unknown/custom protocols', () => { + expect(isSafeUrl('custom:payload')).toBe(false); + }); +}); describe('markdown artifacts', () => { describe('getMarkdownFiles', () => { @@ -41,7 +109,7 @@ describe('markdown artifacts', () => { const markdown = '# Test'; const files = getMarkdownFiles(markdown); - expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('import Markdown from'); + expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('import ReactMarkdown from'); expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('MarkdownRendererProps'); expect(files['/components/ui/MarkdownRenderer.tsx']).toContain( 'export default MarkdownRenderer', @@ -162,13 +230,29 @@ describe('markdown artifacts', () => { }); describe('markdown component structure', () => { - it('should generate a MarkdownRenderer component that uses marked-react', () => { + it('should generate a MarkdownRenderer component with safe markdown rendering', () => { const files = getMarkdownFiles('# Test'); const rendererCode = files['/components/ui/MarkdownRenderer.tsx']; - // Verify the component imports and uses Markdown from marked-react - expect(rendererCode).toContain("import Markdown from 'marked-react'"); - expect(rendererCode).toContain('{content}'); + expect(rendererCode).toContain("import ReactMarkdown from 'react-markdown'"); + expect(rendererCode).toContain("import remarkBreaks from 'remark-breaks'"); + expect(rendererCode).toContain('skipHtml={true}'); + expect(rendererCode).toContain('SAFE_PROTOCOLS'); + expect(rendererCode).toContain('isSafeUrl'); + expect(rendererCode).toContain('urlTransform={urlTransform}'); + expect(rendererCode).toContain('remarkPlugins={remarkPlugins}'); + expect(rendererCode).toContain('isSafeUrl(url) ? url : null'); + }); + + it('should embed isSafeUrl logic matching the exported version', () => { + const files = getMarkdownFiles('# Test'); + const rendererCode = files['/components/ui/MarkdownRenderer.tsx']; + + expect(rendererCode).toContain("new Set(['http:', 'https:', 'mailto:', 'tel:'])"); + expect(rendererCode).toContain('new URL(trimmed).protocol'); + expect(rendererCode).toContain("trimmed.startsWith('/')"); + expect(rendererCode).toContain("trimmed.startsWith('#')"); + expect(rendererCode).toContain("trimmed.startsWith('.')"); }); it('should pass markdown content to the Markdown component', () => { diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index 13f3a23b47..e862d18a40 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -108,7 +108,9 @@ const mermaidDependencies = { }; const markdownDependencies = { - 'marked-react': '^2.0.0', + 'remark-gfm': '^4.0.0', + 'remark-breaks': '^4.0.0', + 'react-markdown': '^9.0.1', }; const dependenciesMap: Record< diff --git a/client/src/utils/markdown.ts b/client/src/utils/markdown.ts index 12556c1a24..24d5105863 100644 --- a/client/src/utils/markdown.ts +++ b/client/src/utils/markdown.ts @@ -1,23 +1,70 @@ import dedent from 'dedent'; -const markdownRenderer = dedent(`import React, { useEffect, useState } from 'react'; -import Markdown from 'marked-react'; +const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']); + +/** + * Allowlist-based URL validator for markdown artifact rendering. + * Mirrored verbatim in the markdownRenderer template string below — + * any logic change MUST be applied to both copies. + */ +export const isSafeUrl = (url: string): boolean => { + const trimmed = url.trim(); + if (!trimmed) { + return false; + } + if (trimmed.startsWith('/') || trimmed.startsWith('#') || trimmed.startsWith('.')) { + return true; + } + try { + return SAFE_PROTOCOLS.has(new URL(trimmed).protocol); + } catch { + return false; + } +}; + +const markdownRenderer = dedent(`import React from 'react'; +import remarkGfm from 'remark-gfm'; +import remarkBreaks from 'remark-breaks'; +import ReactMarkdown from 'react-markdown'; interface MarkdownRendererProps { content: string; } +/** Mirror of the exported isSafeUrl in markdown.ts — keep in sync. */ +const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']); + +const isSafeUrl = (url: string): boolean => { + const trimmed = url.trim(); + if (!trimmed) return false; + if (trimmed.startsWith('/') || trimmed.startsWith('#') || trimmed.startsWith('.')) return true; + try { + return SAFE_PROTOCOLS.has(new URL(trimmed).protocol); + } catch { + return false; + } +}; + +const remarkPlugins = [remarkGfm, remarkBreaks]; +const urlTransform = (url: string) => (isSafeUrl(url) ? url : null); + const MarkdownRenderer: React.FC = ({ content }) => { return (
- {content} + + {content} +
); }; From f7ab5e645ad25ff36463a820d791d277afec985a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 18:41:59 -0400 Subject: [PATCH 045/111] =?UTF-8?q?=F0=9F=AB=B7=20fix:=20Validate=20User-P?= =?UTF-8?q?rovided=20Base=20URL=20in=20Endpoint=20Init=20(#12248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Block SSRF via user-provided baseURL in endpoint initialization User-provided baseURL values (when endpoint is configured with `user_provided`) were passed through to the OpenAI SDK without validation. Combined with `directEndpoint`, this allowed arbitrary server-side requests to internal/metadata URLs. Adds `validateEndpointURL` that checks against known SSRF targets and DNS-resolves hostnames to block private IPs. Applied in both custom and OpenAI endpoint initialization paths. * 🧪 test: Add validateEndpointURL SSRF tests Covers unparseable URLs, localhost, private IPs, link-local/metadata, internal Docker/K8s hostnames, DNS resolution to private IPs, and legitimate public URLs. * 🛡️ fix: Add protocol enforcement and import order fix - Reject non-HTTP/HTTPS schemes (ftp://, file://, data:, etc.) in validateEndpointURL before SSRF hostname checks - Document DNS rebinding limitation and fail-open semantics in JSDoc - Fix import order in custom/initialize.ts per project conventions * 🧪 test: Expand SSRF validation coverage and add initializer integration tests Unit tests for validateEndpointURL: - Non-HTTP/HTTPS schemes (ftp, file, data) - IPv6 loopback, link-local, and unique-local addresses - .local and .internal TLD hostnames - DNS fail-open path (lookup failure allows request) Integration tests for initializeCustom and initializeOpenAI: - Guard fires when userProvidesURL is true - Guard skipped when URL is system-defined or falsy - SSRF rejection propagates and prevents getOpenAIConfig call * 🐛 fix: Correct broken env restore in OpenAI initialize spec process.env was captured by reference, not by value, making the restore closure a no-op. Snapshot individual env keys before mutation so they can be properly restored after each test. * 🛡️ fix: Throw structured ErrorTypes for SSRF base URL validation Replace plain-string Error throws in validateEndpointURL with JSON-structured errors using type 'invalid_base_url' (matching new ErrorTypes.INVALID_BASE_URL enum value). This ensures the client-side Error component can look up a localized message instead of falling through to the raw-text default. Changes across workspaces: - data-provider: add INVALID_BASE_URL to ErrorTypes enum - packages/api: throwInvalidBaseURL helper emits structured JSON - client: add errorMessages entry and localization key - tests: add structured JSON format assertion * 🧹 refactor: Use ErrorTypes enum key in Error.tsx for consistency Replace bare string literal 'invalid_base_url' with computed property [ErrorTypes.INVALID_BASE_URL] to match every other entry in the errorMessages map. --- .../src/components/Messages/Content/Error.tsx | 1 + client/src/locales/en/translation.json | 1 + packages/api/src/auth/domain.spec.ts | 133 +++++++++++++++++ packages/api/src/auth/domain.ts | 42 ++++++ .../src/endpoints/custom/initialize.spec.ts | 119 +++++++++++++++ .../api/src/endpoints/custom/initialize.ts | 7 +- .../src/endpoints/openai/initialize.spec.ts | 135 ++++++++++++++++++ .../api/src/endpoints/openai/initialize.ts | 5 + packages/data-provider/src/config.ts | 4 + 9 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/endpoints/custom/initialize.spec.ts create mode 100644 packages/api/src/endpoints/openai/initialize.spec.ts diff --git a/client/src/components/Messages/Content/Error.tsx b/client/src/components/Messages/Content/Error.tsx index 469e29fe32..ff2f2d7e90 100644 --- a/client/src/components/Messages/Content/Error.tsx +++ b/client/src/components/Messages/Content/Error.tsx @@ -41,6 +41,7 @@ const errorMessages = { [ErrorTypes.NO_USER_KEY]: 'com_error_no_user_key', [ErrorTypes.INVALID_USER_KEY]: 'com_error_invalid_user_key', [ErrorTypes.NO_BASE_URL]: 'com_error_no_base_url', + [ErrorTypes.INVALID_BASE_URL]: 'com_error_invalid_base_url', [ErrorTypes.INVALID_ACTION]: `com_error_${ErrorTypes.INVALID_ACTION}`, [ErrorTypes.INVALID_REQUEST]: `com_error_${ErrorTypes.INVALID_REQUEST}`, [ErrorTypes.REFUSAL]: 'com_error_refusal', diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index f45cdd5f8c..36d882c6a2 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -372,6 +372,7 @@ "com_error_missing_model": "No model selected for {{0}}. Please select a model and try again.", "com_error_models_not_loaded": "Models configuration could not be loaded. Please refresh the page and try again.", "com_error_moderation": "It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We're unable to proceed with this specific topic. If you have any other questions or topics you'd like to explore, please edit your message, or create a new conversation.", + "com_error_invalid_base_url": "The base URL you provided targets a restricted address. Please use a valid external URL and try again.", "com_error_no_base_url": "No base URL found. Please provide one and try again.", "com_error_no_user_key": "No key found. Please provide a key and try again.", "com_error_refusal": "Response refused by safety filters. Rewrite your message and try again. If you encounter this frequently while using Claude Sonnet 4.5 or Opus 4.1, you can try Sonnet 4, which has different usage restrictions.", diff --git a/packages/api/src/auth/domain.spec.ts b/packages/api/src/auth/domain.spec.ts index 76f50213db..a7140528a9 100644 --- a/packages/api/src/auth/domain.spec.ts +++ b/packages/api/src/auth/domain.spec.ts @@ -12,6 +12,7 @@ import { isPrivateIP, isSSRFTarget, resolveHostnameSSRF, + validateEndpointURL, } from './domain'; const mockedLookup = lookup as jest.MockedFunction; @@ -1209,3 +1210,135 @@ describe('isMCPDomainAllowed', () => { }); }); }); + +describe('validateEndpointURL', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should throw for unparseable URLs', async () => { + await expect(validateEndpointURL('not-a-url', 'test-ep')).rejects.toThrow( + 'Invalid base URL for test-ep', + ); + }); + + it('should throw for localhost URLs', async () => { + await expect(validateEndpointURL('http://localhost:8080/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw for private IP URLs', async () => { + await expect(validateEndpointURL('http://192.168.1.1/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + await expect(validateEndpointURL('http://10.0.0.1/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + await expect(validateEndpointURL('http://172.16.0.1/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw for link-local / metadata IP', async () => { + await expect( + validateEndpointURL('http://169.254.169.254/latest/meta-data/', 'test-ep'), + ).rejects.toThrow('targets a restricted address'); + }); + + it('should throw for loopback IP', async () => { + await expect(validateEndpointURL('http://127.0.0.1:11434/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw for internal Docker/Kubernetes hostnames', async () => { + await expect(validateEndpointURL('http://redis:6379/', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + await expect(validateEndpointURL('http://mongodb:27017/', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw when hostname DNS-resolves to a private IP', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '10.0.0.5', family: 4 }] as never); + await expect(validateEndpointURL('https://evil.example.com/v1', 'test-ep')).rejects.toThrow( + 'resolves to a restricted address', + ); + }); + + it('should allow public URLs', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '104.18.7.192', family: 4 }] as never); + await expect( + validateEndpointURL('https://api.openai.com/v1', 'test-ep'), + ).resolves.toBeUndefined(); + }); + + it('should allow public URLs that resolve to public IPs', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '8.8.8.8', family: 4 }] as never); + await expect( + validateEndpointURL('https://api.example.com/v1/chat', 'test-ep'), + ).resolves.toBeUndefined(); + }); + + it('should throw for non-HTTP/HTTPS schemes', async () => { + await expect(validateEndpointURL('ftp://example.com/v1', 'test-ep')).rejects.toThrow( + 'only HTTP and HTTPS are permitted', + ); + await expect(validateEndpointURL('file:///etc/passwd', 'test-ep')).rejects.toThrow( + 'only HTTP and HTTPS are permitted', + ); + await expect(validateEndpointURL('data:text/plain,hello', 'test-ep')).rejects.toThrow( + 'only HTTP and HTTPS are permitted', + ); + }); + + it('should throw for IPv6 loopback URL', async () => { + await expect(validateEndpointURL('http://[::1]:8080/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw for IPv6 link-local URL', async () => { + await expect(validateEndpointURL('http://[fe80::1]/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw for IPv6 unique-local URL', async () => { + await expect(validateEndpointURL('http://[fc00::1]/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw for .local TLD hostname', async () => { + await expect(validateEndpointURL('http://myservice.local/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should throw for .internal TLD hostname', async () => { + await expect(validateEndpointURL('http://api.internal/v1', 'test-ep')).rejects.toThrow( + 'targets a restricted address', + ); + }); + + it('should pass when DNS lookup fails (fail-open)', async () => { + mockedLookup.mockRejectedValueOnce(new Error('ENOTFOUND')); + await expect( + validateEndpointURL('https://nonexistent.example.com/v1', 'test-ep'), + ).resolves.toBeUndefined(); + }); + + it('should throw structured JSON with type invalid_base_url', async () => { + const error = await validateEndpointURL('http://169.254.169.254/latest/', 'my-ep').catch( + (err: Error) => err, + ); + expect(error).toBeInstanceOf(Error); + const parsed = JSON.parse((error as Error).message); + expect(parsed.type).toBe('invalid_base_url'); + expect(parsed.message).toContain('my-ep'); + expect(parsed.message).toContain('targets a restricted address'); + }); +}); diff --git a/packages/api/src/auth/domain.ts b/packages/api/src/auth/domain.ts index 3babb09aa6..fabe2502ff 100644 --- a/packages/api/src/auth/domain.ts +++ b/packages/api/src/auth/domain.ts @@ -499,3 +499,45 @@ export async function isMCPDomainAllowed( // Use MCP_PROTOCOLS (HTTP/HTTPS/WS/WSS) for MCP server validation return isDomainAllowedCore(domain, allowedDomains, MCP_PROTOCOLS); } + +/** Matches ErrorTypes.INVALID_BASE_URL — string literal avoids build-time dependency on data-provider */ +const INVALID_BASE_URL_TYPE = 'invalid_base_url'; + +function throwInvalidBaseURL(message: string): never { + throw new Error(JSON.stringify({ type: INVALID_BASE_URL_TYPE, message })); +} + +/** + * Validates that a user-provided endpoint URL does not target private/internal addresses. + * Throws if the URL is unparseable, uses a non-HTTP(S) scheme, targets a known SSRF hostname, + * or DNS-resolves to a private IP. + * + * @note DNS rebinding: validation performs a single DNS lookup. An adversary controlling + * DNS with TTL=0 could respond with a public IP at validation time and a private IP + * at request time. This is an accepted limitation of point-in-time DNS checks. + * @note Fail-open on DNS errors: a resolution failure here implies a failure at request + * time as well, matching {@link resolveHostnameSSRF} semantics. + */ +export async function validateEndpointURL(url: string, endpoint: string): Promise { + let hostname: string; + let protocol: string; + try { + const parsed = new URL(url); + hostname = parsed.hostname; + protocol = parsed.protocol; + } catch { + throwInvalidBaseURL(`Invalid base URL for ${endpoint}: unable to parse URL.`); + } + + if (protocol !== 'http:' && protocol !== 'https:') { + throwInvalidBaseURL(`Invalid base URL for ${endpoint}: only HTTP and HTTPS are permitted.`); + } + + if (isSSRFTarget(hostname)) { + throwInvalidBaseURL(`Base URL for ${endpoint} targets a restricted address.`); + } + + if (await resolveHostnameSSRF(hostname)) { + throwInvalidBaseURL(`Base URL for ${endpoint} resolves to a restricted address.`); + } +} diff --git a/packages/api/src/endpoints/custom/initialize.spec.ts b/packages/api/src/endpoints/custom/initialize.spec.ts new file mode 100644 index 0000000000..911e17c446 --- /dev/null +++ b/packages/api/src/endpoints/custom/initialize.spec.ts @@ -0,0 +1,119 @@ +import { AuthType } from 'librechat-data-provider'; +import type { BaseInitializeParams } from '~/types'; + +const mockValidateEndpointURL = jest.fn(); +jest.mock('~/auth', () => ({ + validateEndpointURL: (...args: unknown[]) => mockValidateEndpointURL(...args), +})); + +const mockGetOpenAIConfig = jest.fn().mockReturnValue({ + llmConfig: { model: 'test-model' }, + configOptions: {}, +}); +jest.mock('~/endpoints/openai/config', () => ({ + getOpenAIConfig: (...args: unknown[]) => mockGetOpenAIConfig(...args), +})); + +jest.mock('~/endpoints/models', () => ({ + fetchModels: jest.fn(), +})); + +jest.mock('~/cache', () => ({ + standardCache: jest.fn(() => ({ get: jest.fn().mockResolvedValue(null) })), +})); + +jest.mock('~/utils', () => ({ + isUserProvided: (val: string) => val === 'user_provided', + checkUserKeyExpiry: jest.fn(), +})); + +const mockGetCustomEndpointConfig = jest.fn(); +jest.mock('~/app/config', () => ({ + getCustomEndpointConfig: (...args: unknown[]) => mockGetCustomEndpointConfig(...args), +})); + +import { initializeCustom } from './initialize'; + +function createParams(overrides: { + apiKey?: string; + baseURL?: string; + userBaseURL?: string; + userApiKey?: string; + expiresAt?: string; +}): BaseInitializeParams { + const { apiKey = 'sk-test-key', baseURL = 'https://api.example.com/v1' } = overrides; + + mockGetCustomEndpointConfig.mockReturnValue({ + apiKey, + baseURL, + models: {}, + }); + + const db = { + getUserKeyValues: jest.fn().mockResolvedValue({ + apiKey: overrides.userApiKey ?? 'sk-user-key', + baseURL: overrides.userBaseURL ?? 'https://user-api.example.com/v1', + }), + } as unknown as BaseInitializeParams['db']; + + return { + req: { + user: { id: 'user-1' }, + body: { key: overrides.expiresAt ?? '2099-01-01' }, + config: {}, + } as unknown as BaseInitializeParams['req'], + endpoint: 'test-custom', + model_parameters: { model: 'gpt-4' }, + db, + }; +} + +describe('initializeCustom – SSRF guard wiring', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call validateEndpointURL when baseURL is user_provided', async () => { + const params = createParams({ + apiKey: 'sk-test-key', + baseURL: AuthType.USER_PROVIDED, + userBaseURL: 'https://user-api.example.com/v1', + expiresAt: '2099-01-01', + }); + + await initializeCustom(params); + + expect(mockValidateEndpointURL).toHaveBeenCalledTimes(1); + expect(mockValidateEndpointURL).toHaveBeenCalledWith( + 'https://user-api.example.com/v1', + 'test-custom', + ); + }); + + it('should NOT call validateEndpointURL when baseURL is system-defined', async () => { + const params = createParams({ + apiKey: 'sk-test-key', + baseURL: 'https://api.provider.com/v1', + }); + + await initializeCustom(params); + + expect(mockValidateEndpointURL).not.toHaveBeenCalled(); + }); + + it('should propagate SSRF rejection from validateEndpointURL', async () => { + mockValidateEndpointURL.mockRejectedValueOnce( + new Error('Base URL for test-custom targets a restricted address.'), + ); + + const params = createParams({ + apiKey: 'sk-test-key', + baseURL: AuthType.USER_PROVIDED, + userBaseURL: 'http://169.254.169.254/latest/meta-data/', + expiresAt: '2099-01-01', + }); + + await expect(initializeCustom(params)).rejects.toThrow('targets a restricted address'); + expect(mockGetOpenAIConfig).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/endpoints/custom/initialize.ts b/packages/api/src/endpoints/custom/initialize.ts index 7930b1c12f..15b6b873c7 100644 --- a/packages/api/src/endpoints/custom/initialize.ts +++ b/packages/api/src/endpoints/custom/initialize.ts @@ -9,9 +9,10 @@ import type { TEndpoint } from 'librechat-data-provider'; import type { AppConfig } from '@librechat/data-schemas'; import type { BaseInitializeParams, InitializeResultBase, EndpointTokenConfig } from '~/types'; import { getOpenAIConfig } from '~/endpoints/openai/config'; +import { isUserProvided, checkUserKeyExpiry } from '~/utils'; import { getCustomEndpointConfig } from '~/app/config'; import { fetchModels } from '~/endpoints/models'; -import { isUserProvided, checkUserKeyExpiry } from '~/utils'; +import { validateEndpointURL } from '~/auth'; import { standardCache } from '~/cache'; const { PROXY } = process.env; @@ -123,6 +124,10 @@ export async function initializeCustom({ throw new Error(`${endpoint} Base URL not provided.`); } + if (userProvidesURL) { + await validateEndpointURL(baseURL, endpoint); + } + let endpointTokenConfig: EndpointTokenConfig | undefined; const userId = req.user?.id ?? ''; diff --git a/packages/api/src/endpoints/openai/initialize.spec.ts b/packages/api/src/endpoints/openai/initialize.spec.ts new file mode 100644 index 0000000000..ae91571fb3 --- /dev/null +++ b/packages/api/src/endpoints/openai/initialize.spec.ts @@ -0,0 +1,135 @@ +import { AuthType, EModelEndpoint } from 'librechat-data-provider'; +import type { BaseInitializeParams } from '~/types'; + +const mockValidateEndpointURL = jest.fn(); +jest.mock('~/auth', () => ({ + validateEndpointURL: (...args: unknown[]) => mockValidateEndpointURL(...args), +})); + +const mockGetOpenAIConfig = jest.fn().mockReturnValue({ + llmConfig: { model: 'gpt-4' }, + configOptions: {}, +}); +jest.mock('./config', () => ({ + getOpenAIConfig: (...args: unknown[]) => mockGetOpenAIConfig(...args), +})); + +jest.mock('~/utils', () => ({ + getAzureCredentials: jest.fn(), + resolveHeaders: jest.fn(() => ({})), + isUserProvided: (val: string) => val === 'user_provided', + checkUserKeyExpiry: jest.fn(), +})); + +import { initializeOpenAI } from './initialize'; + +function createParams(env: Record): BaseInitializeParams { + const savedEnv: Record = {}; + for (const key of Object.keys(env)) { + savedEnv[key] = process.env[key]; + } + Object.assign(process.env, env); + + const db = { + getUserKeyValues: jest.fn().mockResolvedValue({ + apiKey: 'sk-user-key', + baseURL: 'https://user-proxy.example.com/v1', + }), + } as unknown as BaseInitializeParams['db']; + + const params: BaseInitializeParams = { + req: { + user: { id: 'user-1' }, + body: { key: '2099-01-01' }, + config: { endpoints: {} }, + } as unknown as BaseInitializeParams['req'], + endpoint: EModelEndpoint.openAI, + model_parameters: { model: 'gpt-4' }, + db, + }; + + const restore = () => { + for (const key of Object.keys(env)) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + }; + + return Object.assign(params, { _restore: restore }); +} + +describe('initializeOpenAI – SSRF guard wiring', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call validateEndpointURL when OPENAI_REVERSE_PROXY is user_provided', async () => { + const params = createParams({ + OPENAI_API_KEY: 'sk-test', + OPENAI_REVERSE_PROXY: AuthType.USER_PROVIDED, + }); + + try { + await initializeOpenAI(params); + } finally { + (params as unknown as { _restore: () => void })._restore(); + } + + expect(mockValidateEndpointURL).toHaveBeenCalledTimes(1); + expect(mockValidateEndpointURL).toHaveBeenCalledWith( + 'https://user-proxy.example.com/v1', + EModelEndpoint.openAI, + ); + }); + + it('should NOT call validateEndpointURL when OPENAI_REVERSE_PROXY is a system URL', async () => { + const params = createParams({ + OPENAI_API_KEY: 'sk-test', + OPENAI_REVERSE_PROXY: 'https://api.openai.com/v1', + }); + + try { + await initializeOpenAI(params); + } finally { + (params as unknown as { _restore: () => void })._restore(); + } + + expect(mockValidateEndpointURL).not.toHaveBeenCalled(); + }); + + it('should NOT call validateEndpointURL when baseURL is falsy', async () => { + const params = createParams({ + OPENAI_API_KEY: 'sk-test', + }); + + try { + await initializeOpenAI(params); + } finally { + (params as unknown as { _restore: () => void })._restore(); + } + + expect(mockValidateEndpointURL).not.toHaveBeenCalled(); + }); + + it('should propagate SSRF rejection from validateEndpointURL', async () => { + mockValidateEndpointURL.mockRejectedValueOnce( + new Error('Base URL for openAI targets a restricted address.'), + ); + + const params = createParams({ + OPENAI_API_KEY: 'sk-test', + OPENAI_REVERSE_PROXY: AuthType.USER_PROVIDED, + }); + + try { + await expect(initializeOpenAI(params)).rejects.toThrow('targets a restricted address'); + } finally { + (params as unknown as { _restore: () => void })._restore(); + } + + expect(mockGetOpenAIConfig).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/endpoints/openai/initialize.ts b/packages/api/src/endpoints/openai/initialize.ts index 33ce233d34..a6ad6df895 100644 --- a/packages/api/src/endpoints/openai/initialize.ts +++ b/packages/api/src/endpoints/openai/initialize.ts @@ -6,6 +6,7 @@ import type { UserKeyValues, } from '~/types'; import { getAzureCredentials, resolveHeaders, isUserProvided, checkUserKeyExpiry } from '~/utils'; +import { validateEndpointURL } from '~/auth'; import { getOpenAIConfig } from './config'; /** @@ -55,6 +56,10 @@ export async function initializeOpenAI({ ? userValues?.baseURL : baseURLOptions[endpoint as keyof typeof baseURLOptions]; + if (userProvidesURL && baseURL) { + await validateEndpointURL(baseURL, endpoint); + } + const clientOptions: OpenAIConfigOptions = { proxy: PROXY ?? undefined, reverseProxyUrl: baseURL || undefined, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index e13521c019..bb0c180209 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1560,6 +1560,10 @@ export enum ErrorTypes { * No Base URL Provided. */ NO_BASE_URL = 'no_base_url', + /** + * Base URL targets a restricted or invalid address (SSRF protection). + */ + INVALID_BASE_URL = 'invalid_base_url', /** * Moderation error */ From ad08df4db682b2865f54fd0e77d4706ba9eaf843 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 18:54:34 -0400 Subject: [PATCH 046/111] =?UTF-8?q?=F0=9F=94=8F=20fix:=20Scope=20Agent-Aut?= =?UTF-8?q?hor=20File=20Access=20to=20Attached=20Files=20Only=20(#12251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Scope agent-author file access to attached files only The hasAccessToFilesViaAgent helper short-circuited for agent authors, granting access to all requested file IDs without verifying they were attached to the agent's tool_resources. This enabled an IDOR where any agent author could delete arbitrary files by supplying their agent_id alongside unrelated file IDs. Now both the author and non-author paths check file IDs against the agent's tool_resources before granting access. * chore: Use Object.values/for...of and add JSDoc in getAttachedFileIds * test: Add boundary cases for agent file access authorization - Agent with no tool_resources denies all access (fail-closed) - Files across multiple resource types are all reachable - Author + isDelete: true still scopes to attached files only --- api/models/File.spec.js | 121 +++++++++++++++++++++-- api/server/services/Files/permissions.js | 45 +++++---- 2 files changed, 141 insertions(+), 25 deletions(-) diff --git a/api/models/File.spec.js b/api/models/File.spec.js index 2d4282cff7..ecb2e21b08 100644 --- a/api/models/File.spec.js +++ b/api/models/File.spec.js @@ -152,12 +152,11 @@ describe('File Access Control', () => { expect(accessMap.get(fileIds[3])).toBe(false); }); - it('should grant access to all files when user is the agent author', async () => { + it('should only grant author access to files attached to the agent', async () => { const authorId = new mongoose.Types.ObjectId(); const agentId = uuidv4(); const fileIds = [uuidv4(), uuidv4(), uuidv4()]; - // Create author user await User.create({ _id: authorId, email: 'author@example.com', @@ -165,7 +164,6 @@ describe('File Access Control', () => { provider: 'local', }); - // Create agent await createAgent({ id: agentId, name: 'Test Agent', @@ -174,12 +172,83 @@ describe('File Access Control', () => { provider: 'openai', tool_resources: { file_search: { - file_ids: [fileIds[0]], // Only one file attached + file_ids: [fileIds[0]], + }, + }, + }); + + const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); + const accessMap = await hasAccessToFilesViaAgent({ + userId: authorId, + role: SystemRoles.USER, + fileIds, + agentId, + }); + + expect(accessMap.get(fileIds[0])).toBe(true); + expect(accessMap.get(fileIds[1])).toBe(false); + expect(accessMap.get(fileIds[2])).toBe(false); + }); + + it('should deny all access when agent has no tool_resources', async () => { + const authorId = new mongoose.Types.ObjectId(); + const agentId = uuidv4(); + const fileId = uuidv4(); + + await User.create({ + _id: authorId, + email: 'author-no-resources@example.com', + emailVerified: true, + provider: 'local', + }); + + await createAgent({ + id: agentId, + name: 'Bare Agent', + author: authorId, + model: 'gpt-4', + provider: 'openai', + }); + + const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); + const accessMap = await hasAccessToFilesViaAgent({ + userId: authorId, + role: SystemRoles.USER, + fileIds: [fileId], + agentId, + }); + + expect(accessMap.get(fileId)).toBe(false); + }); + + it('should grant access to files across multiple resource types', async () => { + const authorId = new mongoose.Types.ObjectId(); + const agentId = uuidv4(); + const fileIds = [uuidv4(), uuidv4(), uuidv4()]; + + await User.create({ + _id: authorId, + email: 'author-multi@example.com', + emailVerified: true, + provider: 'local', + }); + + await createAgent({ + id: agentId, + name: 'Multi Resource Agent', + author: authorId, + model: 'gpt-4', + provider: 'openai', + tool_resources: { + file_search: { + file_ids: [fileIds[0]], + }, + execute_code: { + file_ids: [fileIds[1]], }, }, }); - // Check access as the author const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); const accessMap = await hasAccessToFilesViaAgent({ userId: authorId, @@ -188,10 +257,48 @@ describe('File Access Control', () => { agentId, }); - // Author should have access to all files expect(accessMap.get(fileIds[0])).toBe(true); expect(accessMap.get(fileIds[1])).toBe(true); - expect(accessMap.get(fileIds[2])).toBe(true); + expect(accessMap.get(fileIds[2])).toBe(false); + }); + + it('should grant author access to attached files when isDelete is true', async () => { + const authorId = new mongoose.Types.ObjectId(); + const agentId = uuidv4(); + const attachedFileId = uuidv4(); + const unattachedFileId = uuidv4(); + + await User.create({ + _id: authorId, + email: 'author-delete@example.com', + emailVerified: true, + provider: 'local', + }); + + await createAgent({ + id: agentId, + name: 'Delete Test Agent', + author: authorId, + model: 'gpt-4', + provider: 'openai', + tool_resources: { + file_search: { + file_ids: [attachedFileId], + }, + }, + }); + + const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); + const accessMap = await hasAccessToFilesViaAgent({ + userId: authorId, + role: SystemRoles.USER, + fileIds: [attachedFileId, unattachedFileId], + agentId, + isDelete: true, + }); + + expect(accessMap.get(attachedFileId)).toBe(true); + expect(accessMap.get(unattachedFileId)).toBe(false); }); it('should handle non-existent agent gracefully', async () => { diff --git a/api/server/services/Files/permissions.js b/api/server/services/Files/permissions.js index d909afe25a..df484f7c29 100644 --- a/api/server/services/Files/permissions.js +++ b/api/server/services/Files/permissions.js @@ -4,7 +4,26 @@ const { checkPermission } = require('~/server/services/PermissionService'); const { getAgent } = require('~/models/Agent'); /** - * Checks if a user has access to multiple files through a shared agent (batch operation) + * @param {Object} agent - The agent document (lean) + * @returns {Set} All file IDs attached across all resource types + */ +function getAttachedFileIds(agent) { + const attachedFileIds = new Set(); + if (agent.tool_resources) { + for (const resource of Object.values(agent.tool_resources)) { + if (resource?.file_ids && Array.isArray(resource.file_ids)) { + for (const fileId of resource.file_ids) { + attachedFileIds.add(fileId); + } + } + } + } + return attachedFileIds; +} + +/** + * Checks if a user has access to multiple files through a shared agent (batch operation). + * Access is always scoped to files actually attached to the agent's tool_resources. * @param {Object} params - Parameters object * @param {string} params.userId - The user ID to check access for * @param {string} [params.role] - Optional user role to avoid DB query @@ -16,7 +35,6 @@ const { getAgent } = require('~/models/Agent'); const hasAccessToFilesViaAgent = async ({ userId, role, fileIds, agentId, isDelete }) => { const accessMap = new Map(); - // Initialize all files as no access fileIds.forEach((fileId) => accessMap.set(fileId, false)); try { @@ -26,13 +44,17 @@ const hasAccessToFilesViaAgent = async ({ userId, role, fileIds, agentId, isDele return accessMap; } - // Check if user is the author - if so, grant access to all files + const attachedFileIds = getAttachedFileIds(agent); + if (agent.author.toString() === userId.toString()) { - fileIds.forEach((fileId) => accessMap.set(fileId, true)); + fileIds.forEach((fileId) => { + if (attachedFileIds.has(fileId)) { + accessMap.set(fileId, true); + } + }); return accessMap; } - // Check if user has at least VIEW permission on the agent const hasViewPermission = await checkPermission({ userId, role, @@ -46,7 +68,6 @@ const hasAccessToFilesViaAgent = async ({ userId, role, fileIds, agentId, isDele } if (isDelete) { - // Check if user has EDIT permission (which would indicate collaborative access) const hasEditPermission = await checkPermission({ userId, role, @@ -55,23 +76,11 @@ const hasAccessToFilesViaAgent = async ({ userId, role, fileIds, agentId, isDele requiredPermission: PermissionBits.EDIT, }); - // If user only has VIEW permission, they can't access files - // Only users with EDIT permission or higher can access agent files if (!hasEditPermission) { return accessMap; } } - const attachedFileIds = new Set(); - if (agent.tool_resources) { - for (const [_resourceType, resource] of Object.entries(agent.tool_resources)) { - if (resource?.file_ids && Array.isArray(resource.file_ids)) { - resource.file_ids.forEach((fileId) => attachedFileIds.add(fileId)); - } - } - } - - // Grant access only to files that are attached to this agent fileIds.forEach((fileId) => { if (attachedFileIds.has(fileId)) { accessMap.set(fileId, true); From aee1ced81713d06246646709d89dffe56d5f9d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Airam=20Hern=C3=A1ndez=20Hern=C3=A1ndez?= <100208966+Airamhh@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:09:53 +0000 Subject: [PATCH 047/111] =?UTF-8?q?=F0=9F=AA=99=20fix:=20Resolve=20Azure?= =?UTF-8?q?=20AD=20Group=20Overage=20via=20OBO=20Token=20Exchange=20for=20?= =?UTF-8?q?OpenID=20(#12187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Azure AD users belong to 200+ groups, group claims are moved out of the ID token (overage). The existing resolveGroupsFromOverage() called Microsoft Graph directly with the app-audience access token, which Graph rejected (401/403). Changes: - Add exchangeTokenForOverage() dedicated OBO exchange with User.Read scope - Update resolveGroupsFromOverage() to exchange token before Graph call - Add overage handling to OPENID_ADMIN_ROLE block (was silently failing) - Share resolved overage groups between required role and admin role checks - Always resolve via Graph when overage detected (even with partial groups) - Remove debug-only bypass that forced Graph resolution - Add tests for OBO exchange, caching, and admin role overage scenarios Co-authored-by: Airam Hernández Hernández --- api/strategies/openidStrategy.js | 104 ++++++++- api/strategies/openidStrategy.spec.js | 313 +++++++++++++++++++++++++- 2 files changed, 406 insertions(+), 11 deletions(-) diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 0ebdcb04e1..7c43358297 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -315,24 +315,85 @@ function convertToUsername(input, defaultValue = '') { return defaultValue; } +/** + * Exchange the access token for a Graph-scoped token using the On-Behalf-Of (OBO) flow. + * + * The original access token has the app's own audience (api://), which Microsoft Graph + * rejects. This exchange produces a token with audience https://graph.microsoft.com and the + * minimum delegated scope (User.Read) required by /me/getMemberObjects. + * + * Uses a dedicated cache key (`${sub}:overage`) to avoid collisions with other OBO exchanges + * in the codebase (userinfo, Graph principal search). + * + * @param {string} accessToken - The original access token from the OpenID tokenset + * @param {string} sub - The subject identifier for cache keying + * @returns {Promise} A Graph-scoped access token + * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow + */ +async function exchangeTokenForOverage(accessToken, sub) { + if (!openidConfig) { + throw new Error('[openidStrategy] OpenID config not initialized; cannot exchange OBO token'); + } + + const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS); + const cacheKey = `${sub}:overage`; + + const cached = await tokensCache.get(cacheKey); + if (cached?.access_token) { + logger.debug('[openidStrategy] Using cached Graph token for overage resolution'); + return cached.access_token; + } + + const grantResponse = await client.genericGrantRequest( + openidConfig, + 'urn:ietf:params:oauth:grant-type:jwt-bearer', + { + scope: 'https://graph.microsoft.com/User.Read', + assertion: accessToken, + requested_token_use: 'on_behalf_of', + }, + ); + + if (!grantResponse.access_token) { + throw new Error( + '[openidStrategy] OBO exchange succeeded but returned no access_token; cannot call Graph API', + ); + } + + const ttlMs = + Number.isFinite(grantResponse.expires_in) && grantResponse.expires_in > 0 + ? grantResponse.expires_in * 1000 + : 3600 * 1000; + + await tokensCache.set(cacheKey, { access_token: grantResponse.access_token }, ttlMs); + + return grantResponse.access_token; +} + /** * Resolve Azure AD groups when group overage is in effect (groups moved to _claim_names/_claim_sources). * * NOTE: Microsoft recommends treating _claim_names/_claim_sources as a signal only and using Microsoft Graph * to resolve group membership instead of calling the endpoint in _claim_sources directly. * - * @param {string} accessToken - Access token with Microsoft Graph permissions + * Before calling Graph, the access token is exchanged via the OBO flow to obtain a token with the + * correct audience (https://graph.microsoft.com) and User.Read scope. + * + * @param {string} accessToken - Access token from the OpenID tokenset (app audience) + * @param {string} sub - The subject identifier of the user (for OBO exchange and cache keying) * @returns {Promise} Resolved group IDs or null on failure * @see https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference#groups-overage-claim * @see https://learn.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects */ -async function resolveGroupsFromOverage(accessToken) { +async function resolveGroupsFromOverage(accessToken, sub) { try { if (!accessToken) { logger.error('[openidStrategy] Access token missing; cannot resolve group overage'); return null; } + const graphToken = await exchangeTokenForOverage(accessToken, sub); + // Use /me/getMemberObjects so least-privileged delegated permission User.Read is sufficient // when resolving the signed-in user's group membership. const url = 'https://graph.microsoft.com/v1.0/me/getMemberObjects'; @@ -344,7 +405,7 @@ async function resolveGroupsFromOverage(accessToken) { const fetchOptions = { method: 'POST', headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${graphToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ securityEnabledOnly: false }), @@ -364,6 +425,7 @@ async function resolveGroupsFromOverage(accessToken) { } const data = await response.json(); + const values = Array.isArray(data?.value) ? data.value : null; if (!values) { logger.error( @@ -432,6 +494,8 @@ async function processOpenIDAuth(tokenset, existingUsersOnly = false) { const fullName = getFullName(userinfo); const requiredRole = process.env.OPENID_REQUIRED_ROLE; + let resolvedOverageGroups = null; + if (requiredRole) { const requiredRoles = requiredRole .split(',') @@ -451,19 +515,21 @@ async function processOpenIDAuth(tokenset, existingUsersOnly = false) { // Handle Azure AD group overage for ID token groups: when hasgroups or _claim_* indicate overage, // resolve groups via Microsoft Graph instead of relying on token group values. + const hasOverage = + decodedToken?.hasgroups || + (decodedToken?._claim_names?.groups && + decodedToken?._claim_sources?.[decodedToken._claim_names.groups]); + if ( - !Array.isArray(roles) && - typeof roles !== 'string' && requiredRoleTokenKind === 'id' && requiredRoleParameterPath === 'groups' && decodedToken && - (decodedToken.hasgroups || - (decodedToken._claim_names?.groups && - decodedToken._claim_sources?.[decodedToken._claim_names.groups])) + hasOverage ) { - const overageGroups = await resolveGroupsFromOverage(tokenset.access_token); + const overageGroups = await resolveGroupsFromOverage(tokenset.access_token, claims.sub); if (overageGroups) { roles = overageGroups; + resolvedOverageGroups = overageGroups; } } @@ -550,7 +616,25 @@ async function processOpenIDAuth(tokenset, existingUsersOnly = false) { throw new Error('Invalid admin role token kind'); } - const adminRoles = get(adminRoleObject, adminRoleParameterPath); + let adminRoles = get(adminRoleObject, adminRoleParameterPath); + + // Handle Azure AD group overage for admin role when using ID token groups + if (adminRoleTokenKind === 'id' && adminRoleParameterPath === 'groups' && adminRoleObject) { + const hasAdminOverage = + adminRoleObject.hasgroups || + (adminRoleObject._claim_names?.groups && + adminRoleObject._claim_sources?.[adminRoleObject._claim_names.groups]); + + if (hasAdminOverage) { + const overageGroups = + resolvedOverageGroups || + (await resolveGroupsFromOverage(tokenset.access_token, claims.sub)); + if (overageGroups) { + adminRoles = overageGroups; + } + } + } + let adminRoleValues = []; if (Array.isArray(adminRoles)) { adminRoleValues = adminRoles; diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js index 485b77829e..16fa548a59 100644 --- a/api/strategies/openidStrategy.spec.js +++ b/api/strategies/openidStrategy.spec.js @@ -64,6 +64,10 @@ jest.mock('openid-client', () => { // Only return additional properties, but don't override any claims return Promise.resolve({}); }), + genericGrantRequest: jest.fn().mockResolvedValue({ + access_token: 'exchanged_graph_token', + expires_in: 3600, + }), customFetch: Symbol('customFetch'), }; }); @@ -730,7 +734,7 @@ describe('setupOpenId', () => { expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ - Authorization: `Bearer ${tokenset.access_token}`, + Authorization: 'Bearer exchanged_graph_token', }), }), ); @@ -745,6 +749,313 @@ describe('setupOpenId', () => { ); }); + describe('OBO token exchange for overage', () => { + it('exchanges access token via OBO before calling Graph API', async () => { + const openidClient = require('openid-client'); + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['group-required'] }), + }); + + await validate(tokenset); + + expect(openidClient.genericGrantRequest).toHaveBeenCalledWith( + expect.anything(), + 'urn:ietf:params:oauth:grant-type:jwt-bearer', + expect.objectContaining({ + scope: 'https://graph.microsoft.com/User.Read', + assertion: tokenset.access_token, + requested_token_use: 'on_behalf_of', + }), + ); + + expect(undici.fetch).toHaveBeenCalledWith( + 'https://graph.microsoft.com/v1.0/me/getMemberObjects', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer exchanged_graph_token', + }), + }), + ); + }); + + it('caches the exchanged token and reuses it on subsequent calls', async () => { + const openidClient = require('openid-client'); + const getLogStores = require('~/cache/getLogStores'); + const mockSet = jest.fn(); + const mockGet = jest + .fn() + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce({ access_token: 'exchanged_graph_token' }); + getLogStores.mockReturnValue({ get: mockGet, set: mockSet }); + + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['group-required'] }), + }); + + // First call: cache miss → OBO exchange → cache set + await validate(tokenset); + expect(mockSet).toHaveBeenCalledWith( + '1234:overage', + { access_token: 'exchanged_graph_token' }, + 3600000, + ); + expect(openidClient.genericGrantRequest).toHaveBeenCalledTimes(1); + + // Second call: cache hit → no new OBO exchange + openidClient.genericGrantRequest.mockClear(); + await validate(tokenset); + expect(openidClient.genericGrantRequest).not.toHaveBeenCalled(); + }); + }); + + describe('admin role group overage', () => { + it('resolves admin groups via Graph when overage is detected for admin role', async () => { + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + process.env.OPENID_ADMIN_ROLE = 'admin-group-id'; + process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['group-required', 'admin-group-id'] }), + }); + + const { user } = await validate(tokenset); + + expect(user.role).toBe('ADMIN'); + }); + + it('does not grant admin when overage groups do not contain admin role', async () => { + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + process.env.OPENID_ADMIN_ROLE = 'admin-group-id'; + process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['group-required', 'other-group'] }), + }); + + const { user } = await validate(tokenset); + + expect(user).toBeTruthy(); + expect(user.role).toBeUndefined(); + }); + + it('reuses already-resolved overage groups for admin role check (no duplicate Graph call)', async () => { + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + process.env.OPENID_ADMIN_ROLE = 'admin-group-id'; + process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['group-required', 'admin-group-id'] }), + }); + + await validate(tokenset); + + // Graph API should be called only once (for required role), admin role reuses the result + expect(undici.fetch).toHaveBeenCalledTimes(1); + }); + + it('demotes existing admin when overage groups no longer contain admin role', async () => { + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + process.env.OPENID_ADMIN_ROLE = 'admin-group-id'; + process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id'; + + const existingAdminUser = { + _id: 'existingAdminId', + provider: 'openid', + email: tokenset.claims().email, + openidId: tokenset.claims().sub, + username: 'adminuser', + name: 'Admin User', + role: 'ADMIN', + }; + + findUser.mockImplementation(async (query) => { + if (query.openidId === tokenset.claims().sub || query.email === tokenset.claims().email) { + return existingAdminUser; + } + return null; + }); + + jwtDecode.mockReturnValue({ hasgroups: true }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['group-required'] }), + }); + + const { user } = await validate(tokenset); + + expect(user.role).toBe('USER'); + }); + + it('does not attempt overage for admin role when token kind is not id', async () => { + process.env.OPENID_REQUIRED_ROLE = 'requiredRole'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'roles'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + process.env.OPENID_ADMIN_ROLE = 'admin'; + process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'access'; + + jwtDecode.mockReturnValue({ + roles: ['requiredRole'], + hasgroups: true, + }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + const { user } = await validate(tokenset); + + // No Graph call since admin uses access token (not id) + expect(undici.fetch).not.toHaveBeenCalled(); + expect(user.role).toBeUndefined(); + }); + + it('resolves admin via Graph independently when OPENID_REQUIRED_ROLE is not configured', async () => { + delete process.env.OPENID_REQUIRED_ROLE; + process.env.OPENID_ADMIN_ROLE = 'admin-group-id'; + process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['admin-group-id'] }), + }); + + const { user } = await validate(tokenset); + expect(user.role).toBe('ADMIN'); + expect(undici.fetch).toHaveBeenCalledTimes(1); + }); + + it('denies admin when OPENID_REQUIRED_ROLE is absent and Graph does not contain admin group', async () => { + delete process.env.OPENID_REQUIRED_ROLE; + process.env.OPENID_ADMIN_ROLE = 'admin-group-id'; + process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + undici.fetch.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ value: ['other-group'] }), + }); + + const { user } = await validate(tokenset); + expect(user).toBeTruthy(); + expect(user.role).toBeUndefined(); + }); + + it('denies login and logs error when OBO exchange throws', async () => { + const openidClient = require('openid-client'); + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + openidClient.genericGrantRequest.mockRejectedValueOnce(new Error('OBO exchange rejected')); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + const { user, details } = await validate(tokenset); + expect(user).toBe(false); + expect(details.message).toBe('You must have "group-required" role to log in.'); + expect(undici.fetch).not.toHaveBeenCalled(); + }); + + it('denies login when OBO exchange returns no access_token', async () => { + const openidClient = require('openid-client'); + process.env.OPENID_REQUIRED_ROLE = 'group-required'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + + jwtDecode.mockReturnValue({ hasgroups: true }); + openidClient.genericGrantRequest.mockResolvedValueOnce({ expires_in: 3600 }); + + await setupOpenId(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); + + const { user, details } = await validate(tokenset); + expect(user).toBe(false); + expect(details.message).toBe('You must have "group-required" role to log in.'); + expect(undici.fetch).not.toHaveBeenCalled(); + }); + }); + it('should attempt to download and save the avatar if picture is provided', async () => { // Act const { user } = await validate(tokenset); From a26eeea59281889f3d05885bc765c732f1028147 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 20:08:34 -0400 Subject: [PATCH 048/111] =?UTF-8?q?=F0=9F=94=8F=20fix:=20Enforce=20MCP=20S?= =?UTF-8?q?erver=20Authorization=20on=20Agent=20Tool=20Persistence=20(#122?= =?UTF-8?q?50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛡️ fix: Validate MCP tool authorization on agent create/update Agent creation and update accepted arbitrary MCP tool strings without verifying the user has access to the referenced MCP servers. This allowed a user to embed unauthorized server names in tool identifiers (e.g. "anything_mcp_"), causing mcpServerNames to be stored on the agent and granting consumeOnly access via hasAccessViaAgent(). Adds filterAuthorizedTools() that checks MCP tool strings against the user's accessible server configs (via getAllServerConfigs) before persisting. Applied to create, update, and duplicate agent paths. * 🛡️ fix: Harden MCP tool authorization and add test coverage Addresses review findings on the MCP agent tool authorization fix: - Wrap getMCPServersRegistry() in try/catch so uninitialized registry gracefully filters all MCP tools instead of causing a 500 (DoS risk) - Guard revertAgentVersionHandler: filter unauthorized MCP tools after reverting to a previous version snapshot - Preserve existing MCP tools on collaborative updates: only validate newly added tools, preventing silent stripping of tools the editing user lacks direct access to - Add audit logging (logger.warn) when MCP tools are rejected - Refactor to single-pass lazy-fetch (registry queried only on first MCP tool encountered) - Export filterAuthorizedTools for direct unit testing - Add 18 tests covering: authorized/unauthorized/mixed tools, registry unavailable fallback, create/update/duplicate/revert handler paths, collaborative update preservation, and mcpServerNames persistence * test: Add duplicate handler test, use Constants.mcp_delimiter, DB assertions - N1: Add duplicateAgentHandler integration test verifying unauthorized MCP tools are stripped from the cloned agent and mcpServerNames are correctly persisted in the database - N2: Replace all hardcoded '_mcp_' delimiter literals with Constants.mcp_delimiter to prevent silent false-positive tests if the delimiter value ever changes - N3: Add DB state assertion to the revert-with-strip test confirming persisted tools match the response after unauthorized tools are removed * fix: Enforce exact 2-segment format for MCP tool keys Reject MCP tool keys with multiple delimiters to prevent authorization/execution mismatch when `.pop()` vs `split[1]` extract different server names from the same key. * fix: Preserve existing MCP tools when registry is unavailable When the MCP registry is uninitialized (e.g. server restart), existing tools already persisted on the agent are preserved instead of silently stripped. New MCP tools are still rejected when the registry cannot verify them. Applies to duplicate and revert handlers via existingTools param; update handler already preserves existing tools via its diff logic. --- .../agents/filterAuthorizedTools.spec.js | 677 ++++++++++++++++++ api/server/controllers/agents/v1.js | 134 +++- 2 files changed, 801 insertions(+), 10 deletions(-) create mode 100644 api/server/controllers/agents/filterAuthorizedTools.spec.js diff --git a/api/server/controllers/agents/filterAuthorizedTools.spec.js b/api/server/controllers/agents/filterAuthorizedTools.spec.js new file mode 100644 index 0000000000..259e41fb0d --- /dev/null +++ b/api/server/controllers/agents/filterAuthorizedTools.spec.js @@ -0,0 +1,677 @@ +const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); +const { Constants } = require('librechat-data-provider'); +const { agentSchema } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +const d = Constants.mcp_delimiter; + +const mockGetAllServerConfigs = jest.fn(); + +jest.mock('~/server/services/Config', () => ({ + getCachedTools: jest.fn().mockResolvedValue({ + web_search: true, + execute_code: true, + file_search: true, + }), +})); + +jest.mock('~/config', () => ({ + getMCPServersRegistry: jest.fn(() => ({ + getAllServerConfigs: mockGetAllServerConfigs, + })), +})); + +jest.mock('~/models/Project', () => ({ + getProjectByName: jest.fn().mockResolvedValue(null), +})); + +jest.mock('~/server/services/Files/strategies', () => ({ + getStrategyFunctions: jest.fn(), +})); + +jest.mock('~/server/services/Files/images/avatar', () => ({ + resizeAvatar: jest.fn(), +})); + +jest.mock('~/server/services/Files/S3/crud', () => ({ + refreshS3Url: jest.fn(), +})); + +jest.mock('~/server/services/Files/process', () => ({ + filterFile: jest.fn(), +})); + +jest.mock('~/models/Action', () => ({ + updateAction: jest.fn(), + getActions: jest.fn().mockResolvedValue([]), +})); + +jest.mock('~/models/File', () => ({ + deleteFileByFilter: jest.fn(), +})); + +jest.mock('~/server/services/PermissionService', () => ({ + findAccessibleResources: jest.fn().mockResolvedValue([]), + findPubliclyAccessibleResources: jest.fn().mockResolvedValue([]), + grantPermission: jest.fn(), + hasPublicPermission: jest.fn().mockResolvedValue(false), + checkPermission: jest.fn().mockResolvedValue(true), +})); + +jest.mock('~/models', () => ({ + getCategoriesWithCounts: jest.fn(), +})); + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(() => ({ + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + })), +})); + +const { + filterAuthorizedTools, + createAgent: createAgentHandler, + updateAgent: updateAgentHandler, + duplicateAgent: duplicateAgentHandler, + revertAgentVersion: revertAgentVersionHandler, +} = require('./v1'); + +const { getMCPServersRegistry } = require('~/config'); + +let Agent; + +describe('MCP Tool Authorization', () => { + let mongoServer; + let mockReq; + let mockRes; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); + }, 20000); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await Agent.deleteMany({}); + jest.clearAllMocks(); + + getMCPServersRegistry.mockImplementation(() => ({ + getAllServerConfigs: mockGetAllServerConfigs, + })); + mockGetAllServerConfigs.mockResolvedValue({ + authorizedServer: { type: 'sse', url: 'https://authorized.example.com' }, + anotherServer: { type: 'sse', url: 'https://another.example.com' }, + }); + + mockReq = { + user: { + id: new mongoose.Types.ObjectId().toString(), + role: 'USER', + }, + body: {}, + params: {}, + query: {}, + app: { locals: { fileStrategy: 'local' } }, + }; + + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + }); + + describe('filterAuthorizedTools', () => { + const availableTools = { web_search: true, custom_tool: true }; + const userId = 'test-user-123'; + + test('should keep authorized MCP tools and strip unauthorized ones', async () => { + const result = await filterAuthorizedTools({ + tools: [`toolA${d}authorizedServer`, `toolB${d}forbiddenServer`, 'web_search'], + userId, + availableTools, + }); + + expect(result).toContain(`toolA${d}authorizedServer`); + expect(result).toContain('web_search'); + expect(result).not.toContain(`toolB${d}forbiddenServer`); + }); + + test('should keep system tools without querying MCP registry', async () => { + const result = await filterAuthorizedTools({ + tools: ['execute_code', 'file_search', 'web_search'], + userId, + availableTools: {}, + }); + + expect(result).toEqual(['execute_code', 'file_search', 'web_search']); + expect(mockGetAllServerConfigs).not.toHaveBeenCalled(); + }); + + test('should not query MCP registry when no MCP tools are present', async () => { + const result = await filterAuthorizedTools({ + tools: ['web_search', 'custom_tool'], + userId, + availableTools, + }); + + expect(result).toEqual(['web_search', 'custom_tool']); + expect(mockGetAllServerConfigs).not.toHaveBeenCalled(); + }); + + test('should filter all MCP tools when registry is uninitialized', async () => { + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + const result = await filterAuthorizedTools({ + tools: [`toolA${d}someServer`, 'web_search'], + userId, + availableTools, + }); + + expect(result).toEqual(['web_search']); + expect(result).not.toContain(`toolA${d}someServer`); + }); + + test('should handle mixed authorized and unauthorized MCP tools', async () => { + const result = await filterAuthorizedTools({ + tools: [ + 'web_search', + `search${d}authorizedServer`, + `attack${d}victimServer`, + 'execute_code', + `list${d}anotherServer`, + `steal${d}nonexistent`, + ], + userId, + availableTools, + }); + + expect(result).toEqual([ + 'web_search', + `search${d}authorizedServer`, + 'execute_code', + `list${d}anotherServer`, + ]); + }); + + test('should handle empty tools array', async () => { + const result = await filterAuthorizedTools({ + tools: [], + userId, + availableTools, + }); + + expect(result).toEqual([]); + expect(mockGetAllServerConfigs).not.toHaveBeenCalled(); + }); + + test('should handle null/undefined tool entries gracefully', async () => { + const result = await filterAuthorizedTools({ + tools: [null, undefined, '', 'web_search'], + userId, + availableTools, + }); + + expect(result).toEqual(['web_search']); + }); + + test('should call getAllServerConfigs with the correct userId', async () => { + await filterAuthorizedTools({ + tools: [`tool${d}authorizedServer`], + userId: 'specific-user-id', + availableTools, + }); + + expect(mockGetAllServerConfigs).toHaveBeenCalledWith('specific-user-id'); + }); + + test('should only call getAllServerConfigs once even with multiple MCP tools', async () => { + await filterAuthorizedTools({ + tools: [`tool1${d}authorizedServer`, `tool2${d}anotherServer`, `tool3${d}unknownServer`], + userId, + availableTools, + }); + + expect(mockGetAllServerConfigs).toHaveBeenCalledTimes(1); + }); + + test('should preserve existing MCP tools when registry is unavailable', async () => { + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + const existingTools = [`toolA${d}serverA`, `toolB${d}serverB`]; + + const result = await filterAuthorizedTools({ + tools: [...existingTools, `newTool${d}unknownServer`, 'web_search'], + userId, + availableTools, + existingTools, + }); + + expect(result).toContain(`toolA${d}serverA`); + expect(result).toContain(`toolB${d}serverB`); + expect(result).toContain('web_search'); + expect(result).not.toContain(`newTool${d}unknownServer`); + }); + + test('should still reject all MCP tools when registry is unavailable and no existingTools', async () => { + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + const result = await filterAuthorizedTools({ + tools: [`toolA${d}serverA`, 'web_search'], + userId, + availableTools, + }); + + expect(result).toEqual(['web_search']); + }); + + test('should not preserve malformed existing tools when registry is unavailable', async () => { + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + const malformedTool = `a${d}b${d}c`; + const result = await filterAuthorizedTools({ + tools: [malformedTool, `legit${d}serverA`, 'web_search'], + userId, + availableTools, + existingTools: [malformedTool, `legit${d}serverA`], + }); + + expect(result).toContain(`legit${d}serverA`); + expect(result).toContain('web_search'); + expect(result).not.toContain(malformedTool); + }); + + test('should reject malformed MCP tool keys with multiple delimiters', async () => { + const result = await filterAuthorizedTools({ + tools: [ + `attack${d}victimServer${d}authorizedServer`, + `legit${d}authorizedServer`, + `a${d}b${d}c${d}d`, + 'web_search', + ], + userId, + availableTools, + }); + + expect(result).toEqual([`legit${d}authorizedServer`, 'web_search']); + expect(result).not.toContainEqual(expect.stringContaining('victimServer')); + expect(result).not.toContainEqual(expect.stringContaining(`a${d}b`)); + }); + }); + + describe('createAgentHandler - MCP tool authorization', () => { + test('should strip unauthorized MCP tools on create', async () => { + mockReq.body = { + provider: 'openai', + model: 'gpt-4', + name: 'MCP Test Agent', + tools: ['web_search', `validTool${d}authorizedServer`, `attack${d}forbiddenServer`], + }; + + await createAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(201); + const agent = mockRes.json.mock.calls[0][0]; + expect(agent.tools).toContain('web_search'); + expect(agent.tools).toContain(`validTool${d}authorizedServer`); + expect(agent.tools).not.toContain(`attack${d}forbiddenServer`); + }); + + test('should not 500 when MCP registry is uninitialized', async () => { + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + mockReq.body = { + provider: 'openai', + model: 'gpt-4', + name: 'MCP Uninitialized Test', + tools: [`tool${d}someServer`, 'web_search'], + }; + + await createAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(201); + const agent = mockRes.json.mock.calls[0][0]; + expect(agent.tools).toEqual(['web_search']); + }); + + test('should store mcpServerNames only for authorized servers', async () => { + mockReq.body = { + provider: 'openai', + model: 'gpt-4', + name: 'MCP Names Test', + tools: [`toolA${d}authorizedServer`, `toolB${d}forbiddenServer`], + }; + + await createAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(201); + const agent = mockRes.json.mock.calls[0][0]; + const agentInDb = await Agent.findOne({ id: agent.id }); + expect(agentInDb.mcpServerNames).toContain('authorizedServer'); + expect(agentInDb.mcpServerNames).not.toContain('forbiddenServer'); + }); + }); + + describe('updateAgentHandler - MCP tool authorization', () => { + let existingAgentId; + let existingAgentAuthorId; + + beforeEach(async () => { + existingAgentAuthorId = new mongoose.Types.ObjectId(); + const agent = await Agent.create({ + id: `agent_${uuidv4()}`, + name: 'Original Agent', + provider: 'openai', + model: 'gpt-4', + author: existingAgentAuthorId, + tools: ['web_search', `existingTool${d}authorizedServer`], + mcpServerNames: ['authorizedServer'], + versions: [ + { + name: 'Original Agent', + provider: 'openai', + model: 'gpt-4', + tools: ['web_search', `existingTool${d}authorizedServer`], + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + existingAgentId = agent.id; + }); + + test('should preserve existing MCP tools even if editor lacks access', async () => { + mockGetAllServerConfigs.mockResolvedValue({}); + + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { + tools: ['web_search', `existingTool${d}authorizedServer`], + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const updatedAgent = mockRes.json.mock.calls[0][0]; + expect(updatedAgent.tools).toContain(`existingTool${d}authorizedServer`); + expect(updatedAgent.tools).toContain('web_search'); + }); + + test('should reject newly added unauthorized MCP tools', async () => { + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { + tools: ['web_search', `existingTool${d}authorizedServer`, `attack${d}forbiddenServer`], + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const updatedAgent = mockRes.json.mock.calls[0][0]; + expect(updatedAgent.tools).toContain('web_search'); + expect(updatedAgent.tools).toContain(`existingTool${d}authorizedServer`); + expect(updatedAgent.tools).not.toContain(`attack${d}forbiddenServer`); + }); + + test('should allow adding authorized MCP tools', async () => { + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { + tools: ['web_search', `existingTool${d}authorizedServer`, `newTool${d}anotherServer`], + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const updatedAgent = mockRes.json.mock.calls[0][0]; + expect(updatedAgent.tools).toContain(`newTool${d}anotherServer`); + }); + + test('should not query MCP registry when no new MCP tools added', async () => { + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { + tools: ['web_search', `existingTool${d}authorizedServer`], + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockGetAllServerConfigs).not.toHaveBeenCalled(); + }); + + test('should preserve existing MCP tools when registry unavailable and user edits agent', async () => { + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { + name: 'Renamed After Restart', + tools: ['web_search', `existingTool${d}authorizedServer`], + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const updatedAgent = mockRes.json.mock.calls[0][0]; + expect(updatedAgent.tools).toContain(`existingTool${d}authorizedServer`); + expect(updatedAgent.tools).toContain('web_search'); + expect(updatedAgent.name).toBe('Renamed After Restart'); + }); + + test('should preserve existing MCP tools when server not in configs (disconnected)', async () => { + mockGetAllServerConfigs.mockResolvedValue({}); + + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { + name: 'Edited While Disconnected', + tools: ['web_search', `existingTool${d}authorizedServer`], + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const updatedAgent = mockRes.json.mock.calls[0][0]; + expect(updatedAgent.tools).toContain(`existingTool${d}authorizedServer`); + expect(updatedAgent.name).toBe('Edited While Disconnected'); + }); + }); + + describe('duplicateAgentHandler - MCP tool authorization', () => { + let sourceAgentId; + let sourceAgentAuthorId; + + beforeEach(async () => { + sourceAgentAuthorId = new mongoose.Types.ObjectId(); + const agent = await Agent.create({ + id: `agent_${uuidv4()}`, + name: 'Source Agent', + provider: 'openai', + model: 'gpt-4', + author: sourceAgentAuthorId, + tools: ['web_search', `tool${d}authorizedServer`, `tool${d}forbiddenServer`], + mcpServerNames: ['authorizedServer', 'forbiddenServer'], + versions: [ + { + name: 'Source Agent', + provider: 'openai', + model: 'gpt-4', + tools: ['web_search', `tool${d}authorizedServer`, `tool${d}forbiddenServer`], + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + sourceAgentId = agent.id; + }); + + test('should strip unauthorized MCP tools from duplicated agent', async () => { + mockGetAllServerConfigs.mockResolvedValue({ + authorizedServer: { type: 'sse' }, + }); + + mockReq.user.id = sourceAgentAuthorId.toString(); + mockReq.params.id = sourceAgentId; + + await duplicateAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(201); + const { agent: newAgent } = mockRes.json.mock.calls[0][0]; + expect(newAgent.id).not.toBe(sourceAgentId); + expect(newAgent.tools).toContain('web_search'); + expect(newAgent.tools).toContain(`tool${d}authorizedServer`); + expect(newAgent.tools).not.toContain(`tool${d}forbiddenServer`); + + const agentInDb = await Agent.findOne({ id: newAgent.id }); + expect(agentInDb.mcpServerNames).toContain('authorizedServer'); + expect(agentInDb.mcpServerNames).not.toContain('forbiddenServer'); + }); + + test('should preserve source agent MCP tools when registry is unavailable', async () => { + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + mockReq.user.id = sourceAgentAuthorId.toString(); + mockReq.params.id = sourceAgentId; + + await duplicateAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(201); + const { agent: newAgent } = mockRes.json.mock.calls[0][0]; + expect(newAgent.tools).toContain('web_search'); + expect(newAgent.tools).toContain(`tool${d}authorizedServer`); + expect(newAgent.tools).toContain(`tool${d}forbiddenServer`); + }); + }); + + describe('revertAgentVersionHandler - MCP tool authorization', () => { + let existingAgentId; + let existingAgentAuthorId; + + beforeEach(async () => { + existingAgentAuthorId = new mongoose.Types.ObjectId(); + const agent = await Agent.create({ + id: `agent_${uuidv4()}`, + name: 'Reverted Agent V2', + provider: 'openai', + model: 'gpt-4', + author: existingAgentAuthorId, + tools: ['web_search'], + versions: [ + { + name: 'Reverted Agent V1', + provider: 'openai', + model: 'gpt-4', + tools: ['web_search', `oldTool${d}revokedServer`], + createdAt: new Date(Date.now() - 10000), + updatedAt: new Date(Date.now() - 10000), + }, + { + name: 'Reverted Agent V2', + provider: 'openai', + model: 'gpt-4', + tools: ['web_search'], + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + existingAgentId = agent.id; + }); + + test('should strip unauthorized MCP tools after reverting to a previous version', async () => { + mockGetAllServerConfigs.mockResolvedValue({ + authorizedServer: { type: 'sse' }, + }); + + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { version_index: 0 }; + + await revertAgentVersionHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const result = mockRes.json.mock.calls[0][0]; + expect(result.tools).toContain('web_search'); + expect(result.tools).not.toContain(`oldTool${d}revokedServer`); + + const agentInDb = await Agent.findOne({ id: existingAgentId }); + expect(agentInDb.tools).toContain('web_search'); + expect(agentInDb.tools).not.toContain(`oldTool${d}revokedServer`); + }); + + test('should keep authorized MCP tools after revert', async () => { + await Agent.updateOne( + { id: existingAgentId }, + { $set: { 'versions.0.tools': ['web_search', `tool${d}authorizedServer`] } }, + ); + + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { version_index: 0 }; + + await revertAgentVersionHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const result = mockRes.json.mock.calls[0][0]; + expect(result.tools).toContain('web_search'); + expect(result.tools).toContain(`tool${d}authorizedServer`); + }); + + test('should preserve version MCP tools when registry is unavailable on revert', async () => { + await Agent.updateOne( + { id: existingAgentId }, + { + $set: { + 'versions.0.tools': [ + 'web_search', + `validTool${d}authorizedServer`, + `otherTool${d}anotherServer`, + ], + }, + }, + ); + + getMCPServersRegistry.mockImplementation(() => { + throw new Error('MCPServersRegistry has not been initialized.'); + }); + + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { version_index: 0 }; + + await revertAgentVersionHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const result = mockRes.json.mock.calls[0][0]; + expect(result.tools).toContain('web_search'); + expect(result.tools).toContain(`validTool${d}authorizedServer`); + expect(result.tools).toContain(`otherTool${d}anotherServer`); + + const agentInDb = await Agent.findOne({ id: existingAgentId }); + expect(agentInDb.tools).toContain(`validTool${d}authorizedServer`); + expect(agentInDb.tools).toContain(`otherTool${d}anotherServer`); + }); + }); +}); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index dbb97df24b..309873e56c 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -49,6 +49,7 @@ const { refreshS3Url } = require('~/server/services/Files/S3/crud'); const { filterFile } = require('~/server/services/Files/process'); const { updateAction, getActions } = require('~/models/Action'); const { getCachedTools } = require('~/server/services/Config'); +const { getMCPServersRegistry } = require('~/config'); const { getLogStores } = require('~/cache'); const systemTools = { @@ -98,6 +99,78 @@ const validateEdgeAgentAccess = async (edges, userId, userRole) => { .map((a) => a.id); }; +/** + * Filters tools to only include those the user is authorized to use. + * MCP tools must match the exact format `{toolName}_mcp_{serverName}` (exactly 2 segments). + * Multi-delimiter keys are rejected to prevent authorization/execution mismatch. + * Non-MCP tools must appear in availableTools (global tool cache) or systemTools. + * + * When `existingTools` is provided and the MCP registry is unavailable (e.g. server restart), + * tools already present on the agent are preserved rather than stripped — they were validated + * when originally added, and we cannot re-verify them without the registry. + * @param {object} params + * @param {string[]} params.tools - Raw tool strings from the request + * @param {string} params.userId - Requesting user ID for MCP server access check + * @param {Record} params.availableTools - Global non-MCP tool cache + * @param {string[]} [params.existingTools] - Tools already persisted on the agent document + * @returns {Promise} Only the authorized subset of tools + */ +const filterAuthorizedTools = async ({ tools, userId, availableTools, existingTools }) => { + const filteredTools = []; + let mcpServerConfigs; + let registryUnavailable = false; + const existingToolSet = existingTools?.length ? new Set(existingTools) : null; + + for (const tool of tools) { + if (availableTools[tool] || systemTools[tool]) { + filteredTools.push(tool); + continue; + } + + if (!tool?.includes(Constants.mcp_delimiter)) { + continue; + } + + if (mcpServerConfigs === undefined) { + try { + mcpServerConfigs = (await getMCPServersRegistry().getAllServerConfigs(userId)) ?? {}; + } catch (e) { + logger.warn( + '[filterAuthorizedTools] MCP registry unavailable, filtering all MCP tools', + e.message, + ); + mcpServerConfigs = {}; + registryUnavailable = true; + } + } + + const parts = tool.split(Constants.mcp_delimiter); + if (parts.length !== 2) { + logger.warn( + `[filterAuthorizedTools] Rejected malformed MCP tool key "${tool}" for user ${userId}`, + ); + continue; + } + + if (registryUnavailable && existingToolSet?.has(tool)) { + filteredTools.push(tool); + continue; + } + + const [, serverName] = parts; + if (!serverName || !Object.hasOwn(mcpServerConfigs, serverName)) { + logger.warn( + `[filterAuthorizedTools] Rejected MCP tool "${tool}" — server "${serverName}" not accessible to user ${userId}`, + ); + continue; + } + + filteredTools.push(tool); + } + + return filteredTools; +}; + /** * Creates an Agent. * @route POST /Agents @@ -132,15 +205,7 @@ const createAgentHandler = async (req, res) => { agentData.tools = []; const availableTools = (await getCachedTools()) ?? {}; - for (const tool of tools) { - if (availableTools[tool]) { - agentData.tools.push(tool); - } else if (systemTools[tool]) { - agentData.tools.push(tool); - } else if (tool.includes(Constants.mcp_delimiter)) { - agentData.tools.push(tool); - } - } + agentData.tools = await filterAuthorizedTools({ tools, userId, availableTools }); const agent = await createAgent(agentData); @@ -322,6 +387,26 @@ const updateAgentHandler = async (req, res) => { updateData.tools = ocrConversion.tools; } + if (updateData.tools) { + const existingToolSet = new Set(existingAgent.tools ?? []); + const newMCPTools = updateData.tools.filter( + (t) => !existingToolSet.has(t) && t?.includes(Constants.mcp_delimiter), + ); + + if (newMCPTools.length > 0) { + const availableTools = (await getCachedTools()) ?? {}; + const approvedNew = await filterAuthorizedTools({ + tools: newMCPTools, + userId: req.user.id, + availableTools, + }); + const rejectedSet = new Set(newMCPTools.filter((t) => !approvedNew.includes(t))); + if (rejectedSet.size > 0) { + updateData.tools = updateData.tools.filter((t) => !rejectedSet.has(t)); + } + } + } + let updatedAgent = Object.keys(updateData).length > 0 ? await updateAgent({ id }, updateData, { @@ -464,6 +549,17 @@ const duplicateAgentHandler = async (req, res) => { const agentActions = await Promise.all(promises); newAgentData.actions = agentActions; + + if (newAgentData.tools?.length) { + const availableTools = (await getCachedTools()) ?? {}; + newAgentData.tools = await filterAuthorizedTools({ + tools: newAgentData.tools, + userId, + availableTools, + existingTools: newAgentData.tools, + }); + } + const newAgent = await createAgent(newAgentData); try { @@ -792,7 +888,24 @@ const revertAgentVersionHandler = async (req, res) => { // Permissions are enforced via route middleware (ACL EDIT) - const updatedAgent = await revertAgentVersion({ id }, version_index); + let updatedAgent = await revertAgentVersion({ id }, version_index); + + if (updatedAgent.tools?.length) { + const availableTools = (await getCachedTools()) ?? {}; + const filteredTools = await filterAuthorizedTools({ + tools: updatedAgent.tools, + userId: req.user.id, + availableTools, + existingTools: updatedAgent.tools, + }); + if (filteredTools.length !== updatedAgent.tools.length) { + updatedAgent = await updateAgent( + { id }, + { tools: filteredTools }, + { updatingUserId: req.user.id }, + ); + } + } if (updatedAgent.author) { updatedAgent.author = updatedAgent.author.toString(); @@ -860,4 +973,5 @@ module.exports = { uploadAgentAvatar: uploadAgentAvatarHandler, revertAgentVersion: revertAgentVersionHandler, getAgentCategories, + filterAuthorizedTools, }; From 6f87b49df8fa2696dc54d52a7a0ab117da8dbd60 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 23:01:36 -0400 Subject: [PATCH 049/111] =?UTF-8?q?=F0=9F=9B=82=20fix:=20Enforce=20Actions?= =?UTF-8?q?=20Capability=20Gate=20Across=20All=20Event-Driven=20Tool=20Loa?= =?UTF-8?q?ding=20Paths=20(#12252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: gate action tools by actions capability in all code paths Extract resolveAgentCapabilities helper to eliminate 3x-duplicated capability resolution. Apply early action-tool filtering in both loadToolDefinitionsWrapper and loadAgentTools non-definitions path. Gate loadActionToolsForExecution in loadToolsForExecution behind an actionsEnabled parameter with a cache-based fallback. Replace the late capability guard in loadAgentTools with a hasActionTools check to avoid unnecessary loadActionSets DB calls and duplicate warnings. * fix: thread actionsEnabled through InitializedAgent type Add actionsEnabled to the loadTools callback return type, InitializedAgent, and the initializeAgent destructuring/return so callers can forward the resolved value to loadToolsForExecution without redundant getEndpointsConfig cache lookups. * fix: pass actionsEnabled from callers to loadToolsForExecution Thread actionsEnabled through the agentToolContexts map in initialize.js (primary and handoff agents) and through primaryConfig in the openai.js and responses.js controllers, avoiding per-tool-call capability re-resolution on the hot path. * test: add regression tests for action capability gating Test the real exported functions (resolveAgentCapabilities, loadAgentTools, loadToolsForExecution) with mocked dependencies instead of shadow re-implementations. Covers definition filtering, execution gating, actionsEnabled param forwarding, and fallback capability resolution. * test: use Constants.EPHEMERAL_AGENT_ID in ephemeral fallback test Replaces a string guess with the canonical constant to avoid fragility if the ephemeral detection heuristic changes. * fix: populate agentToolContexts for addedConvo parallel agents After processAddedConvo returns, backfill agentToolContexts for any agents in agentConfigs not already present, so ON_TOOL_EXECUTE for added-convo agents receives actionsEnabled instead of falling back to a per-call cache lookup. --- api/server/controllers/agents/openai.js | 1 + api/server/controllers/agents/responses.js | 2 + .../services/Endpoints/agents/initialize.js | 16 + api/server/services/ToolService.js | 73 ++-- .../services/__tests__/ToolService.spec.js | 312 +++++++++++++++++- packages/api/src/agents/initialize.ts | 6 + 6 files changed, 372 insertions(+), 38 deletions(-) diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index e8561f15fe..bab81f1535 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -265,6 +265,7 @@ const OpenAIChatCompletionController = async (req, res) => { toolRegistry: primaryConfig.toolRegistry, userMCPAuthMap: primaryConfig.userMCPAuthMap, tool_resources: primaryConfig.tool_resources, + actionsEnabled: primaryConfig.actionsEnabled, }); }, toolEndCallback, diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index 83e6ad6efd..bbf02580dd 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -429,6 +429,7 @@ const createResponse = async (req, res) => { toolRegistry: primaryConfig.toolRegistry, userMCPAuthMap: primaryConfig.userMCPAuthMap, tool_resources: primaryConfig.tool_resources, + actionsEnabled: primaryConfig.actionsEnabled, }); }, toolEndCallback, @@ -586,6 +587,7 @@ const createResponse = async (req, res) => { toolRegistry: primaryConfig.toolRegistry, userMCPAuthMap: primaryConfig.userMCPAuthMap, tool_resources: primaryConfig.tool_resources, + actionsEnabled: primaryConfig.actionsEnabled, }); }, toolEndCallback, diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 44583e6dbc..762236ea19 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -128,6 +128,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { toolRegistry: ctx.toolRegistry, userMCPAuthMap: ctx.userMCPAuthMap, tool_resources: ctx.tool_resources, + actionsEnabled: ctx.actionsEnabled, }); logger.debug(`[ON_TOOL_EXECUTE] loaded ${result.loadedTools?.length ?? 0} tools`); @@ -214,6 +215,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { toolRegistry: primaryConfig.toolRegistry, userMCPAuthMap: primaryConfig.userMCPAuthMap, tool_resources: primaryConfig.tool_resources, + actionsEnabled: primaryConfig.actionsEnabled, }); const agent_ids = primaryConfig.agent_ids; @@ -297,6 +299,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { toolRegistry: config.toolRegistry, userMCPAuthMap: config.userMCPAuthMap, tool_resources: config.tool_resources, + actionsEnabled: config.actionsEnabled, }); agentConfigs.set(agentId, config); @@ -370,6 +373,19 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { userMCPAuthMap = updatedMCPAuthMap; } + for (const [agentId, config] of agentConfigs) { + if (agentToolContexts.has(agentId)) { + continue; + } + agentToolContexts.set(agentId, { + agent: config, + toolRegistry: config.toolRegistry, + userMCPAuthMap: config.userMCPAuthMap, + tool_resources: config.tool_resources, + actionsEnabled: config.actionsEnabled, + }); + } + // Ensure edges is an array when we have multiple agents (multi-agent mode) // MultiAgentGraph.categorizeEdges requires edges to be iterable if (agentConfigs.size > 0 && !edges) { diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 62499348e6..5fc95e748d 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -64,6 +64,26 @@ const { redactMessage } = require('~/config/parsers'); const { findPluginAuthsByKeys } = require('~/models'); const { getFlowStateManager } = require('~/config'); const { getLogStores } = require('~/cache'); + +/** + * Resolves the set of enabled agent capabilities from endpoints config, + * falling back to app-level or default capabilities for ephemeral agents. + * @param {ServerRequest} req + * @param {Object} appConfig + * @param {string} agentId + * @returns {Promise>} + */ +async function resolveAgentCapabilities(req, appConfig, agentId) { + const endpointsConfig = await getEndpointsConfig(req); + let capabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []); + if (capabilities.size === 0 && isEphemeralAgentId(agentId)) { + capabilities = new Set( + appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, + ); + } + return capabilities; +} + /** * Processes the required actions by calling the appropriate tools and returning the outputs. * @param {OpenAIClient} client - OpenAI or StreamRunManager Client. @@ -445,17 +465,11 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to } const appConfig = req.config; - const endpointsConfig = await getEndpointsConfig(req); - let enabledCapabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []); - - if (enabledCapabilities.size === 0 && isEphemeralAgentId(agent.id)) { - enabledCapabilities = new Set( - appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, - ); - } + const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent.id); const checkCapability = (capability) => enabledCapabilities.has(capability); const areToolsEnabled = checkCapability(AgentCapabilities.tools); + const actionsEnabled = checkCapability(AgentCapabilities.actions); const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools); const filteredTools = agent.tools?.filter((tool) => { @@ -468,7 +482,10 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to if (tool === Tools.web_search) { return checkCapability(AgentCapabilities.web_search); } - if (!areToolsEnabled && !tool.includes(actionDelimiter)) { + if (tool.includes(actionDelimiter)) { + return actionsEnabled; + } + if (!areToolsEnabled) { return false; } return true; @@ -765,6 +782,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to toolContextMap, toolDefinitions, hasDeferredTools, + actionsEnabled, }; } @@ -808,14 +826,7 @@ async function loadAgentTools({ } const appConfig = req.config; - const endpointsConfig = await getEndpointsConfig(req); - let enabledCapabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []); - /** Edge case: use defined/fallback capabilities when the "agents" endpoint is not enabled */ - if (enabledCapabilities.size === 0 && isEphemeralAgentId(agent.id)) { - enabledCapabilities = new Set( - appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, - ); - } + const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent.id); const checkCapability = (capability) => { const enabled = enabledCapabilities.has(capability); if (!enabled) { @@ -832,6 +843,7 @@ async function loadAgentTools({ return enabled; }; const areToolsEnabled = checkCapability(AgentCapabilities.tools); + const actionsEnabled = checkCapability(AgentCapabilities.actions); let includesWebSearch = false; const _agentTools = agent.tools?.filter((tool) => { @@ -842,7 +854,9 @@ async function loadAgentTools({ } else if (tool === Tools.web_search) { includesWebSearch = checkCapability(AgentCapabilities.web_search); return includesWebSearch; - } else if (!areToolsEnabled && !tool.includes(actionDelimiter)) { + } else if (tool.includes(actionDelimiter)) { + return actionsEnabled; + } else if (!areToolsEnabled) { return false; } return true; @@ -947,13 +961,15 @@ async function loadAgentTools({ agentTools.push(...additionalTools); - if (!checkCapability(AgentCapabilities.actions)) { + const hasActionTools = _agentTools.some((t) => t.includes(actionDelimiter)); + if (!hasActionTools) { return { toolRegistry, userMCPAuthMap, toolContextMap, toolDefinitions, hasDeferredTools, + actionsEnabled, tools: agentTools, }; } @@ -969,6 +985,7 @@ async function loadAgentTools({ toolContextMap, toolDefinitions, hasDeferredTools, + actionsEnabled, tools: agentTools, }; } @@ -1101,6 +1118,7 @@ async function loadAgentTools({ userMCPAuthMap, toolDefinitions, hasDeferredTools, + actionsEnabled, tools: agentTools, }; } @@ -1118,9 +1136,11 @@ async function loadAgentTools({ * @param {AbortSignal} [params.signal] - Abort signal * @param {Object} params.agent - The agent object * @param {string[]} params.toolNames - Names of tools to load + * @param {Map} [params.toolRegistry] - Tool registry * @param {Record>} [params.userMCPAuthMap] - User MCP auth map * @param {Object} [params.tool_resources] - Tool resources * @param {string|null} [params.streamId] - Stream ID for web search callbacks + * @param {boolean} [params.actionsEnabled] - Whether the actions capability is enabled * @returns {Promise<{ loadedTools: Array, configurable: Object }>} */ async function loadToolsForExecution({ @@ -1133,11 +1153,17 @@ async function loadToolsForExecution({ userMCPAuthMap, tool_resources, streamId = null, + actionsEnabled, }) { const appConfig = req.config; const allLoadedTools = []; const configurable = { userMCPAuthMap }; + if (actionsEnabled === undefined) { + const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id); + actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions); + } + const isToolSearch = toolNames.includes(AgentConstants.TOOL_SEARCH); const isPTC = toolNames.includes(AgentConstants.PROGRAMMATIC_TOOL_CALLING); @@ -1194,7 +1220,6 @@ async function loadToolsForExecution({ const actionToolNames = allToolNamesToLoad.filter((name) => name.includes(actionDelimiter)); const regularToolNames = allToolNamesToLoad.filter((name) => !name.includes(actionDelimiter)); - /** @type {Record} */ if (regularToolNames.length > 0) { const includesWebSearch = regularToolNames.includes(Tools.web_search); const webSearchCallbacks = includesWebSearch ? createOnSearchResults(res, streamId) : undefined; @@ -1225,7 +1250,7 @@ async function loadToolsForExecution({ } } - if (actionToolNames.length > 0 && agent) { + if (actionToolNames.length > 0 && agent && actionsEnabled) { const actionTools = await loadActionToolsForExecution({ req, res, @@ -1235,6 +1260,11 @@ async function loadToolsForExecution({ actionToolNames, }); allLoadedTools.push(...actionTools); + } else if (actionToolNames.length > 0 && agent && !actionsEnabled) { + logger.warn( + `[loadToolsForExecution] Capability "${AgentCapabilities.actions}" disabled. ` + + `Skipping action tool execution. User: ${req.user.id} | Agent: ${agent.id} | Tools: ${actionToolNames.join(', ')}`, + ); } if (isPTC && allLoadedTools.length > 0) { @@ -1395,4 +1425,5 @@ module.exports = { loadAgentTools, loadToolsForExecution, processRequiredActions, + resolveAgentCapabilities, }; diff --git a/api/server/services/__tests__/ToolService.spec.js b/api/server/services/__tests__/ToolService.spec.js index c44298b09c..a468a88eb3 100644 --- a/api/server/services/__tests__/ToolService.spec.js +++ b/api/server/services/__tests__/ToolService.spec.js @@ -1,19 +1,304 @@ const { + Tools, Constants, + EModelEndpoint, + actionDelimiter, AgentCapabilities, defaultAgentCapabilities, } = require('librechat-data-provider'); -/** - * Tests for ToolService capability checking logic. - * The actual loadAgentTools function has many dependencies, so we test - * the capability checking logic in isolation. - */ -describe('ToolService - Capability Checking', () => { +const mockGetEndpointsConfig = jest.fn(); +const mockGetMCPServerTools = jest.fn(); +const mockGetCachedTools = jest.fn(); +jest.mock('~/server/services/Config', () => ({ + getEndpointsConfig: (...args) => mockGetEndpointsConfig(...args), + getMCPServerTools: (...args) => mockGetMCPServerTools(...args), + getCachedTools: (...args) => mockGetCachedTools(...args), +})); + +const mockLoadToolDefinitions = jest.fn(); +const mockGetUserMCPAuthMap = jest.fn(); +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), + loadToolDefinitions: (...args) => mockLoadToolDefinitions(...args), + getUserMCPAuthMap: (...args) => mockGetUserMCPAuthMap(...args), +})); + +const mockLoadToolsUtil = jest.fn(); +jest.mock('~/app/clients/tools/util', () => ({ + loadTools: (...args) => mockLoadToolsUtil(...args), +})); + +const mockLoadActionSets = jest.fn(); +jest.mock('~/server/services/Tools/credentials', () => ({ + loadAuthValues: jest.fn().mockResolvedValue({}), +})); +jest.mock('~/server/services/Tools/search', () => ({ + createOnSearchResults: jest.fn(), +})); +jest.mock('~/server/services/Tools/mcp', () => ({ + reinitMCPServer: jest.fn(), +})); +jest.mock('~/server/services/Files/process', () => ({ + processFileURL: jest.fn(), + uploadImageBuffer: jest.fn(), +})); +jest.mock('~/app/clients/tools/util/fileSearch', () => ({ + primeFiles: jest.fn().mockResolvedValue({}), +})); +jest.mock('~/server/services/Files/Code/process', () => ({ + primeFiles: jest.fn().mockResolvedValue({}), +})); +jest.mock('../ActionService', () => ({ + loadActionSets: (...args) => mockLoadActionSets(...args), + decryptMetadata: jest.fn(), + createActionTool: jest.fn(), + domainParser: jest.fn(), +})); +jest.mock('~/server/services/Threads', () => ({ + recordUsage: jest.fn(), +})); +jest.mock('~/models', () => ({ + findPluginAuthsByKeys: jest.fn(), +})); +jest.mock('~/config', () => ({ + getFlowStateManager: jest.fn(() => ({})), +})); +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(() => ({})), +})); + +const { + loadAgentTools, + loadToolsForExecution, + resolveAgentCapabilities, +} = require('../ToolService'); + +function createMockReq(capabilities) { + return { + user: { id: 'user_123' }, + config: { + endpoints: { + [EModelEndpoint.agents]: { + capabilities, + }, + }, + }, + }; +} + +function createEndpointsConfig(capabilities) { + return { + [EModelEndpoint.agents]: { capabilities }, + }; +} + +describe('ToolService - Action Capability Gating', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockLoadToolDefinitions.mockResolvedValue({ + toolDefinitions: [], + toolRegistry: new Map(), + hasDeferredTools: false, + }); + mockLoadToolsUtil.mockResolvedValue({ loadedTools: [], toolContextMap: {} }); + mockLoadActionSets.mockResolvedValue([]); + }); + + describe('resolveAgentCapabilities', () => { + it('should return capabilities from endpoints config', async () => { + const capabilities = [AgentCapabilities.tools, AgentCapabilities.actions]; + const req = createMockReq(capabilities); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + const result = await resolveAgentCapabilities(req, req.config, 'agent_123'); + + expect(result).toBeInstanceOf(Set); + expect(result.has(AgentCapabilities.tools)).toBe(true); + expect(result.has(AgentCapabilities.actions)).toBe(true); + expect(result.has(AgentCapabilities.web_search)).toBe(false); + }); + + it('should fall back to default capabilities for ephemeral agents with empty config', async () => { + const req = createMockReq(defaultAgentCapabilities); + mockGetEndpointsConfig.mockResolvedValue({}); + + const result = await resolveAgentCapabilities(req, req.config, Constants.EPHEMERAL_AGENT_ID); + + for (const cap of defaultAgentCapabilities) { + expect(result.has(cap)).toBe(true); + } + }); + + it('should return empty set when no capabilities and not ephemeral', async () => { + const req = createMockReq([]); + mockGetEndpointsConfig.mockResolvedValue({}); + + const result = await resolveAgentCapabilities(req, req.config, 'agent_123'); + + expect(result.size).toBe(0); + }); + }); + + describe('loadAgentTools (definitionsOnly=true) — action tool filtering', () => { + const actionToolName = `get_weather${actionDelimiter}api_example_com`; + const regularTool = 'calculator'; + + it('should exclude action tools from definitions when actions capability is disabled', async () => { + const capabilities = [AgentCapabilities.tools, AgentCapabilities.web_search]; + const req = createMockReq(capabilities); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + await loadAgentTools({ + req, + res: {}, + agent: { id: 'agent_123', tools: [regularTool, actionToolName] }, + definitionsOnly: true, + }); + + expect(mockLoadToolDefinitions).toHaveBeenCalledTimes(1); + const [callArgs] = mockLoadToolDefinitions.mock.calls[0]; + expect(callArgs.tools).toContain(regularTool); + expect(callArgs.tools).not.toContain(actionToolName); + }); + + it('should include action tools in definitions when actions capability is enabled', async () => { + const capabilities = [AgentCapabilities.tools, AgentCapabilities.actions]; + const req = createMockReq(capabilities); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + await loadAgentTools({ + req, + res: {}, + agent: { id: 'agent_123', tools: [regularTool, actionToolName] }, + definitionsOnly: true, + }); + + expect(mockLoadToolDefinitions).toHaveBeenCalledTimes(1); + const [callArgs] = mockLoadToolDefinitions.mock.calls[0]; + expect(callArgs.tools).toContain(regularTool); + expect(callArgs.tools).toContain(actionToolName); + }); + + it('should return actionsEnabled in the result', async () => { + const capabilities = [AgentCapabilities.tools]; + const req = createMockReq(capabilities); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + const result = await loadAgentTools({ + req, + res: {}, + agent: { id: 'agent_123', tools: [regularTool] }, + definitionsOnly: true, + }); + + expect(result.actionsEnabled).toBe(false); + }); + }); + + describe('loadAgentTools (definitionsOnly=false) — action tool filtering', () => { + const actionToolName = `get_weather${actionDelimiter}api_example_com`; + const regularTool = 'calculator'; + + it('should not load action sets when actions capability is disabled', async () => { + const capabilities = [AgentCapabilities.tools, AgentCapabilities.web_search]; + const req = createMockReq(capabilities); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + await loadAgentTools({ + req, + res: {}, + agent: { id: 'agent_123', tools: [regularTool, actionToolName] }, + definitionsOnly: false, + }); + + expect(mockLoadActionSets).not.toHaveBeenCalled(); + }); + + it('should load action sets when actions capability is enabled and action tools present', async () => { + const capabilities = [AgentCapabilities.tools, AgentCapabilities.actions]; + const req = createMockReq(capabilities); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + await loadAgentTools({ + req, + res: {}, + agent: { id: 'agent_123', tools: [regularTool, actionToolName] }, + definitionsOnly: false, + }); + + expect(mockLoadActionSets).toHaveBeenCalledWith({ agent_id: 'agent_123' }); + }); + }); + + describe('loadToolsForExecution — action tool gating', () => { + const actionToolName = `get_weather${actionDelimiter}api_example_com`; + const regularTool = Tools.web_search; + + it('should skip action tool loading when actionsEnabled=false', async () => { + const req = createMockReq([]); + req.config = {}; + + const result = await loadToolsForExecution({ + req, + res: {}, + agent: { id: 'agent_123' }, + toolNames: [regularTool, actionToolName], + actionsEnabled: false, + }); + + expect(mockLoadActionSets).not.toHaveBeenCalled(); + expect(result.loadedTools).toBeDefined(); + }); + + it('should load action tools when actionsEnabled=true', async () => { + const req = createMockReq([AgentCapabilities.actions]); + req.config = {}; + + await loadToolsForExecution({ + req, + res: {}, + agent: { id: 'agent_123' }, + toolNames: [actionToolName], + actionsEnabled: true, + }); + + expect(mockLoadActionSets).toHaveBeenCalledWith({ agent_id: 'agent_123' }); + }); + + it('should resolve actionsEnabled from capabilities when not explicitly provided', async () => { + const capabilities = [AgentCapabilities.tools]; + const req = createMockReq(capabilities); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + await loadToolsForExecution({ + req, + res: {}, + agent: { id: 'agent_123' }, + toolNames: [actionToolName], + }); + + expect(mockGetEndpointsConfig).toHaveBeenCalled(); + expect(mockLoadActionSets).not.toHaveBeenCalled(); + }); + + it('should not call loadActionSets when there are no action tools', async () => { + const req = createMockReq([AgentCapabilities.actions]); + req.config = {}; + + await loadToolsForExecution({ + req, + res: {}, + agent: { id: 'agent_123' }, + toolNames: [regularTool], + actionsEnabled: true, + }); + + expect(mockLoadActionSets).not.toHaveBeenCalled(); + }); + }); + describe('checkCapability logic', () => { - /** - * Simulates the checkCapability function from loadAgentTools - */ const createCheckCapability = (enabledCapabilities, logger = { warn: jest.fn() }) => { return (capability) => { const enabled = enabledCapabilities.has(capability); @@ -124,10 +409,6 @@ describe('ToolService - Capability Checking', () => { }); describe('userMCPAuthMap gating', () => { - /** - * Simulates the guard condition used in both loadToolDefinitionsWrapper - * and loadAgentTools to decide whether getUserMCPAuthMap should be called. - */ const shouldFetchMCPAuth = (tools) => tools?.some((t) => t.includes(Constants.mcp_delimiter)) ?? false; @@ -178,20 +459,17 @@ describe('ToolService - Capability Checking', () => { return (capability) => enabledCapabilities.has(capability); }; - // When deferred_tools is in capabilities const withDeferred = new Set([AgentCapabilities.deferred_tools, AgentCapabilities.tools]); const checkWithDeferred = createCheckCapability(withDeferred); expect(checkWithDeferred(AgentCapabilities.deferred_tools)).toBe(true); - // When deferred_tools is NOT in capabilities const withoutDeferred = new Set([AgentCapabilities.tools, AgentCapabilities.actions]); const checkWithoutDeferred = createCheckCapability(withoutDeferred); expect(checkWithoutDeferred(AgentCapabilities.deferred_tools)).toBe(false); }); it('should use defaultAgentCapabilities when no capabilities configured', () => { - // Simulates the fallback behavior in loadAgentTools - const endpointsConfig = {}; // No capabilities configured + const endpointsConfig = {}; const enabledCapabilities = new Set( endpointsConfig?.capabilities ?? defaultAgentCapabilities, ); diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index af604beb81..913835a007 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -52,6 +52,8 @@ export type InitializedAgent = Agent & { toolDefinitions?: LCTool[]; /** Precomputed flag indicating if any tools have defer_loading enabled (for efficient runtime checks) */ hasDeferredTools?: boolean; + /** Whether the actions capability is enabled (resolved during tool loading) */ + actionsEnabled?: boolean; }; /** @@ -90,6 +92,7 @@ export interface InitializeAgentParams { /** Serializable tool definitions for event-driven mode */ toolDefinitions?: LCTool[]; hasDeferredTools?: boolean; + actionsEnabled?: boolean; } | null>; /** Endpoint option (contains model_parameters and endpoint info) */ endpointOption?: Partial; @@ -283,6 +286,7 @@ export async function initializeAgent( userMCPAuthMap, toolDefinitions, hasDeferredTools, + actionsEnabled, tools: structuredTools, } = (await loadTools?.({ req, @@ -300,6 +304,7 @@ export async function initializeAgent( toolRegistry: undefined, toolDefinitions: [], hasDeferredTools: false, + actionsEnabled: undefined, }; const { getOptions, overrideProvider } = getProviderConfig({ @@ -409,6 +414,7 @@ export async function initializeAgent( userMCPAuthMap, toolDefinitions, hasDeferredTools, + actionsEnabled, attachments: finalAttachments, toolContextMap: toolContextMap ?? {}, useLegacyContent: !!options.useLegacyContent, From 8e8fb01d18b7471607b4e3bd4a894ae135d3cfaa Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 23:02:36 -0400 Subject: [PATCH 050/111] =?UTF-8?q?=F0=9F=A7=B1=20fix:=20Enforce=20Agent?= =?UTF-8?q?=20Access=20Control=20on=20Context=20and=20OCR=20File=20Loading?= =?UTF-8?q?=20(#12253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔏 fix: Apply agent access control filtering to context/OCR resource loading The context/OCR file path in primeResources fetched files by file_id without applying filterFilesByAgentAccess, unlike the file_search and execute_code paths. Add filterFiles dependency injection to primeResources and invoke it after getFiles to enforce consistent access control. * fix: Wire filterFilesByAgentAccess into all agent initialization callers Pass the filterFilesByAgentAccess function from the JS layer into the TS initializeAgent → primeResources chain via dependency injection, covering primary, handoff, added-convo, and memory agent init paths. * test: Add access control filtering tests for primeResources Cover filterFiles invocation with context/OCR files, verify filtering rejects inaccessible files, and confirm graceful fallback when filterFiles, userId, or agentId are absent. * fix: Guard filterFilesByAgentAccess against ephemeral agent IDs Ephemeral agents have no DB document, so getAgent returns null and the access map defaults to all-false, silently blocking all non-owned files. Short-circuit with isEphemeralAgentId to preserve the pass-through behavior for inline-built agents (memory, tool agents). * fix: Clean up resources.ts and JS caller import order Remove redundant optional chain on req.user.role inside user-guarded block, update primeResources JSDoc with filterFiles and agentId params, and reorder JS imports to longest-to-shortest per project conventions. * test: Strengthen OCR assertion and add filterFiles error-path test Use toHaveBeenCalledWith for the OCR filtering test to verify exact arguments after the OCR→context merge step. Add test for filterFiles rejection to verify graceful degradation (logs error, returns original tool_resources). * fix: Correct import order in addedConvo.js and initialize.js Sort by total line length descending: loadAddedAgent (91) before filterFilesByAgentAccess (84), loadAgentTools (91) before filterFilesByAgentAccess (84). * test: Add unit tests for filterFilesByAgentAccess and hasAccessToFilesViaAgent Cover every branch in permissions.js: ephemeral agent guard, missing userId/agentId/files early returns, all-owned short-circuit, mixed owned + non-owned with VIEW/no-VIEW, agent-not-found fail-closed, author path scoped to attached files, EDIT gate on delete, DB error fail-closed, and agent with no tool_resources. * test: Cover file.user undefined/null in permissions spec Files with no user field fall into the non-owned path and get run through hasAccessToFilesViaAgent. Add two cases: attached file with no user field is returned, unattached file with no user field is excluded. --- api/server/controllers/agents/client.js | 2 + .../services/Endpoints/agents/addedConvo.js | 2 + .../services/Endpoints/agents/initialize.js | 3 + api/server/services/Files/permissions.js | 4 +- api/server/services/Files/permissions.spec.js | 409 ++++++++++++++++++ packages/api/src/agents/initialize.ts | 6 +- packages/api/src/agents/resources.test.ts | 275 +++++++++++- packages/api/src/agents/resources.ts | 32 +- 8 files changed, 708 insertions(+), 25 deletions(-) create mode 100644 api/server/services/Files/permissions.spec.js diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 0ecd62b819..c454bd65cf 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -44,6 +44,7 @@ const { isEphemeralAgentId, removeNullishValues, } = require('librechat-data-provider'); +const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); const { encodeAndFormat } = require('~/server/services/Files/images/encode'); const { updateBalance, bulkInsertTransactions } = require('~/models'); @@ -479,6 +480,7 @@ class AgentClient extends BaseClient { getUserKeyValues: db.getUserKeyValues, getToolFilesByIds: db.getToolFilesByIds, getCodeGeneratedFiles: db.getCodeGeneratedFiles, + filterFilesByAgentAccess, }, ); diff --git a/api/server/services/Endpoints/agents/addedConvo.js b/api/server/services/Endpoints/agents/addedConvo.js index 25b1327991..11b87e450e 100644 --- a/api/server/services/Endpoints/agents/addedConvo.js +++ b/api/server/services/Endpoints/agents/addedConvo.js @@ -1,6 +1,7 @@ const { logger } = require('@librechat/data-schemas'); const { initializeAgent, validateAgentModel } = require('@librechat/api'); const { loadAddedAgent, setGetAgent, ADDED_AGENT_ID } = require('~/models/loadAddedAgent'); +const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); const { getConvoFiles } = require('~/models/Conversation'); const { getAgent } = require('~/models/Agent'); const db = require('~/models'); @@ -108,6 +109,7 @@ const processAddedConvo = async ({ getUserKeyValues: db.getUserKeyValues, getToolFilesByIds: db.getToolFilesByIds, getCodeGeneratedFiles: db.getCodeGeneratedFiles, + filterFilesByAgentAccess, }, ); diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 762236ea19..08f631c3d2 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -22,6 +22,7 @@ const { getDefaultHandlers, } = require('~/server/controllers/agents/callbacks'); const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); +const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); const { getModelsConfig } = require('~/server/controllers/ModelController'); const { checkPermission } = require('~/server/services/PermissionService'); const AgentClient = require('~/server/controllers/agents/client'); @@ -204,6 +205,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { getUserCodeFiles: db.getUserCodeFiles, getToolFilesByIds: db.getToolFilesByIds, getCodeGeneratedFiles: db.getCodeGeneratedFiles, + filterFilesByAgentAccess, }, ); @@ -284,6 +286,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { getUserCodeFiles: db.getUserCodeFiles, getToolFilesByIds: db.getToolFilesByIds, getCodeGeneratedFiles: db.getCodeGeneratedFiles, + filterFilesByAgentAccess, }, ); diff --git a/api/server/services/Files/permissions.js b/api/server/services/Files/permissions.js index df484f7c29..b9a5d6656f 100644 --- a/api/server/services/Files/permissions.js +++ b/api/server/services/Files/permissions.js @@ -1,5 +1,5 @@ const { logger } = require('@librechat/data-schemas'); -const { PermissionBits, ResourceType } = require('librechat-data-provider'); +const { PermissionBits, ResourceType, isEphemeralAgentId } = require('librechat-data-provider'); const { checkPermission } = require('~/server/services/PermissionService'); const { getAgent } = require('~/models/Agent'); @@ -104,7 +104,7 @@ const hasAccessToFilesViaAgent = async ({ userId, role, fileIds, agentId, isDele * @returns {Promise>} Filtered array of accessible files */ const filterFilesByAgentAccess = async ({ files, userId, role, agentId }) => { - if (!userId || !agentId || !files || files.length === 0) { + if (!userId || !agentId || !files || files.length === 0 || isEphemeralAgentId(agentId)) { return files; } diff --git a/api/server/services/Files/permissions.spec.js b/api/server/services/Files/permissions.spec.js new file mode 100644 index 0000000000..85e7b2dc5b --- /dev/null +++ b/api/server/services/Files/permissions.spec.js @@ -0,0 +1,409 @@ +jest.mock('@librechat/data-schemas', () => ({ + logger: { error: jest.fn() }, +})); + +jest.mock('~/server/services/PermissionService', () => ({ + checkPermission: jest.fn(), +})); + +jest.mock('~/models/Agent', () => ({ + getAgent: jest.fn(), +})); + +const { logger } = require('@librechat/data-schemas'); +const { Constants, PermissionBits, ResourceType } = require('librechat-data-provider'); +const { checkPermission } = require('~/server/services/PermissionService'); +const { getAgent } = require('~/models/Agent'); +const { filterFilesByAgentAccess, hasAccessToFilesViaAgent } = require('./permissions'); + +const AUTHOR_ID = 'author-user-id'; +const USER_ID = 'viewer-user-id'; +const AGENT_ID = 'agent_test-abc123'; +const AGENT_MONGO_ID = 'mongo-agent-id'; + +function makeFile(file_id, user) { + return { file_id, user, filename: `${file_id}.txt` }; +} + +function makeAgent(overrides = {}) { + return { + _id: AGENT_MONGO_ID, + id: AGENT_ID, + author: AUTHOR_ID, + tool_resources: { + file_search: { file_ids: ['attached-1', 'attached-2'] }, + execute_code: { file_ids: ['attached-3'] }, + }, + ...overrides, + }; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('filterFilesByAgentAccess', () => { + describe('early returns (no DB calls)', () => { + it('should return files unfiltered for ephemeral agentId', async () => { + const files = [makeFile('f1', 'other-user')]; + const result = await filterFilesByAgentAccess({ + files, + userId: USER_ID, + agentId: Constants.EPHEMERAL_AGENT_ID, + }); + + expect(result).toBe(files); + expect(getAgent).not.toHaveBeenCalled(); + }); + + it('should return files unfiltered for non-agent_ prefixed agentId', async () => { + const files = [makeFile('f1', 'other-user')]; + const result = await filterFilesByAgentAccess({ + files, + userId: USER_ID, + agentId: 'custom-memory-id', + }); + + expect(result).toBe(files); + expect(getAgent).not.toHaveBeenCalled(); + }); + + it('should return files when userId is missing', async () => { + const files = [makeFile('f1', 'someone')]; + const result = await filterFilesByAgentAccess({ + files, + userId: undefined, + agentId: AGENT_ID, + }); + + expect(result).toBe(files); + expect(getAgent).not.toHaveBeenCalled(); + }); + + it('should return files when agentId is missing', async () => { + const files = [makeFile('f1', 'someone')]; + const result = await filterFilesByAgentAccess({ + files, + userId: USER_ID, + agentId: undefined, + }); + + expect(result).toBe(files); + expect(getAgent).not.toHaveBeenCalled(); + }); + + it('should return empty array when files is empty', async () => { + const result = await filterFilesByAgentAccess({ + files: [], + userId: USER_ID, + agentId: AGENT_ID, + }); + + expect(result).toEqual([]); + expect(getAgent).not.toHaveBeenCalled(); + }); + + it('should return undefined when files is nullish', async () => { + const result = await filterFilesByAgentAccess({ + files: null, + userId: USER_ID, + agentId: AGENT_ID, + }); + + expect(result).toBeNull(); + expect(getAgent).not.toHaveBeenCalled(); + }); + }); + + describe('all files owned by userId', () => { + it('should return all files without calling getAgent', async () => { + const files = [makeFile('f1', USER_ID), makeFile('f2', USER_ID)]; + const result = await filterFilesByAgentAccess({ + files, + userId: USER_ID, + agentId: AGENT_ID, + }); + + expect(result).toEqual(files); + expect(getAgent).not.toHaveBeenCalled(); + }); + }); + + describe('mixed owned and non-owned files', () => { + const ownedFile = makeFile('owned-1', USER_ID); + const sharedFile = makeFile('attached-1', AUTHOR_ID); + const unattachedFile = makeFile('not-attached', AUTHOR_ID); + + it('should return owned + accessible non-owned files when user has VIEW', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(true); + + const result = await filterFilesByAgentAccess({ + files: [ownedFile, sharedFile, unattachedFile], + userId: USER_ID, + role: 'USER', + agentId: AGENT_ID, + }); + + expect(result).toHaveLength(2); + expect(result.map((f) => f.file_id)).toContain('owned-1'); + expect(result.map((f) => f.file_id)).toContain('attached-1'); + expect(result.map((f) => f.file_id)).not.toContain('not-attached'); + }); + + it('should return only owned files when user lacks VIEW permission', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(false); + + const result = await filterFilesByAgentAccess({ + files: [ownedFile, sharedFile], + userId: USER_ID, + role: 'USER', + agentId: AGENT_ID, + }); + + expect(result).toEqual([ownedFile]); + }); + + it('should return only owned files when agent is not found', async () => { + getAgent.mockResolvedValue(null); + + const result = await filterFilesByAgentAccess({ + files: [ownedFile, sharedFile], + userId: USER_ID, + agentId: AGENT_ID, + }); + + expect(result).toEqual([ownedFile]); + }); + + it('should return only owned files on DB error (fail-closed)', async () => { + getAgent.mockRejectedValue(new Error('DB connection lost')); + + const result = await filterFilesByAgentAccess({ + files: [ownedFile, sharedFile], + userId: USER_ID, + agentId: AGENT_ID, + }); + + expect(result).toEqual([ownedFile]); + expect(logger.error).toHaveBeenCalled(); + }); + }); + + describe('file with no user field', () => { + it('should treat file as non-owned and run through access check', async () => { + const noUserFile = makeFile('attached-1', undefined); + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(true); + + const result = await filterFilesByAgentAccess({ + files: [noUserFile], + userId: USER_ID, + role: 'USER', + agentId: AGENT_ID, + }); + + expect(getAgent).toHaveBeenCalled(); + expect(result).toEqual([noUserFile]); + }); + + it('should exclude file with no user field when not attached to agent', async () => { + const noUserFile = makeFile('not-attached', null); + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(true); + + const result = await filterFilesByAgentAccess({ + files: [noUserFile], + userId: USER_ID, + role: 'USER', + agentId: AGENT_ID, + }); + + expect(result).toEqual([]); + }); + }); + + describe('no owned files (all non-owned)', () => { + const file1 = makeFile('attached-1', AUTHOR_ID); + const file2 = makeFile('not-attached', AUTHOR_ID); + + it('should return only attached files when user has VIEW', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(true); + + const result = await filterFilesByAgentAccess({ + files: [file1, file2], + userId: USER_ID, + role: 'USER', + agentId: AGENT_ID, + }); + + expect(result).toEqual([file1]); + }); + + it('should return empty array when no VIEW permission', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(false); + + const result = await filterFilesByAgentAccess({ + files: [file1, file2], + userId: USER_ID, + agentId: AGENT_ID, + }); + + expect(result).toEqual([]); + }); + + it('should return empty array when agent not found', async () => { + getAgent.mockResolvedValue(null); + + const result = await filterFilesByAgentAccess({ + files: [file1], + userId: USER_ID, + agentId: AGENT_ID, + }); + + expect(result).toEqual([]); + }); + }); +}); + +describe('hasAccessToFilesViaAgent', () => { + describe('agent not found', () => { + it('should return all-false map', async () => { + getAgent.mockResolvedValue(null); + + const result = await hasAccessToFilesViaAgent({ + userId: USER_ID, + fileIds: ['f1', 'f2'], + agentId: AGENT_ID, + }); + + expect(result.get('f1')).toBe(false); + expect(result.get('f2')).toBe(false); + }); + }); + + describe('author path', () => { + it('should grant access to attached files for the agent author', async () => { + getAgent.mockResolvedValue(makeAgent()); + + const result = await hasAccessToFilesViaAgent({ + userId: AUTHOR_ID, + fileIds: ['attached-1', 'not-attached'], + agentId: AGENT_ID, + }); + + expect(result.get('attached-1')).toBe(true); + expect(result.get('not-attached')).toBe(false); + expect(checkPermission).not.toHaveBeenCalled(); + }); + }); + + describe('VIEW permission path', () => { + it('should grant access to attached files for viewer with VIEW permission', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(true); + + const result = await hasAccessToFilesViaAgent({ + userId: USER_ID, + role: 'USER', + fileIds: ['attached-1', 'attached-3', 'not-attached'], + agentId: AGENT_ID, + }); + + expect(result.get('attached-1')).toBe(true); + expect(result.get('attached-3')).toBe(true); + expect(result.get('not-attached')).toBe(false); + + expect(checkPermission).toHaveBeenCalledWith({ + userId: USER_ID, + role: 'USER', + resourceType: ResourceType.AGENT, + resourceId: AGENT_MONGO_ID, + requiredPermission: PermissionBits.VIEW, + }); + }); + + it('should deny all when VIEW permission is missing', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValue(false); + + const result = await hasAccessToFilesViaAgent({ + userId: USER_ID, + fileIds: ['attached-1'], + agentId: AGENT_ID, + }); + + expect(result.get('attached-1')).toBe(false); + }); + }); + + describe('delete path (EDIT permission required)', () => { + it('should grant access when both VIEW and EDIT pass', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValueOnce(true).mockResolvedValueOnce(true); + + const result = await hasAccessToFilesViaAgent({ + userId: USER_ID, + fileIds: ['attached-1'], + agentId: AGENT_ID, + isDelete: true, + }); + + expect(result.get('attached-1')).toBe(true); + expect(checkPermission).toHaveBeenCalledTimes(2); + expect(checkPermission).toHaveBeenLastCalledWith( + expect.objectContaining({ requiredPermission: PermissionBits.EDIT }), + ); + }); + + it('should deny all when VIEW passes but EDIT fails', async () => { + getAgent.mockResolvedValue(makeAgent()); + checkPermission.mockResolvedValueOnce(true).mockResolvedValueOnce(false); + + const result = await hasAccessToFilesViaAgent({ + userId: USER_ID, + fileIds: ['attached-1'], + agentId: AGENT_ID, + isDelete: true, + }); + + expect(result.get('attached-1')).toBe(false); + }); + }); + + describe('error handling', () => { + it('should return all-false map on DB error (fail-closed)', async () => { + getAgent.mockRejectedValue(new Error('connection refused')); + + const result = await hasAccessToFilesViaAgent({ + userId: USER_ID, + fileIds: ['f1', 'f2'], + agentId: AGENT_ID, + }); + + expect(result.get('f1')).toBe(false); + expect(result.get('f2')).toBe(false); + expect(logger.error).toHaveBeenCalledWith( + '[hasAccessToFilesViaAgent] Error checking file access:', + expect.any(Error), + ); + }); + }); + + describe('agent with no tool_resources', () => { + it('should deny all files even for the author', async () => { + getAgent.mockResolvedValue(makeAgent({ tool_resources: undefined })); + + const result = await hasAccessToFilesViaAgent({ + userId: AUTHOR_ID, + fileIds: ['f1'], + agentId: AGENT_ID, + }); + + expect(result.get('f1')).toBe(false); + }); + }); +}); diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 913835a007..d5bfca5aba 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -31,6 +31,7 @@ import { filterFilesByEndpointConfig } from '~/files'; import { generateArtifactsPrompt } from '~/prompts'; import { getProviderConfig } from '~/endpoints'; import { primeResources } from './resources'; +import type { TFilterFilesByAgentAccess } from './resources'; /** * Extended agent type with additional fields needed after initialization @@ -111,7 +112,9 @@ export interface InitializeAgentDbMethods extends EndpointDbMethods { /** Update usage tracking for multiple files */ updateFilesUsage: (files: Array<{ file_id: string }>, fileIds?: string[]) => Promise; /** Get files from database */ - getFiles: (filter: unknown, sort: unknown, select: unknown, opts?: unknown) => Promise; + getFiles: (filter: unknown, sort: unknown, select: unknown) => Promise; + /** Filter files by agent access permissions (ownership or agent attachment) */ + filterFilesByAgentAccess?: TFilterFilesByAgentAccess; /** Get tool files by IDs (user-uploaded files only, code files handled separately) */ getToolFilesByIds: (fileIds: string[], toolSet: Set) => Promise; /** Get conversation file IDs */ @@ -271,6 +274,7 @@ export async function initializeAgent( const { attachments: primedAttachments, tool_resources } = await primeResources({ req: req as never, getFiles: db.getFiles as never, + filterFiles: db.filterFilesByAgentAccess, appConfig: req.config, agentId: agent.id, attachments: currentFiles diff --git a/packages/api/src/agents/resources.test.ts b/packages/api/src/agents/resources.test.ts index bfd2327764..641fb9284c 100644 --- a/packages/api/src/agents/resources.test.ts +++ b/packages/api/src/agents/resources.test.ts @@ -4,7 +4,7 @@ import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-dat import type { TAgentsEndpoint, TFile } from 'librechat-data-provider'; import type { IUser, AppConfig } from '@librechat/data-schemas'; import type { Request as ServerRequest } from 'express'; -import type { TGetFiles } from './resources'; +import type { TGetFiles, TFilterFilesByAgentAccess } from './resources'; // Mock logger jest.mock('@librechat/data-schemas', () => ({ @@ -17,16 +17,16 @@ describe('primeResources', () => { let mockReq: ServerRequest & { user?: IUser }; let mockAppConfig: AppConfig; let mockGetFiles: jest.MockedFunction; + let mockFilterFiles: jest.MockedFunction; let requestFileSet: Set; beforeEach(() => { - // Reset mocks jest.clearAllMocks(); - // Setup mock request - mockReq = {} as unknown as ServerRequest & { user?: IUser }; + mockReq = { + user: { id: 'user1', role: 'USER' }, + } as unknown as ServerRequest & { user?: IUser }; - // Setup mock appConfig mockAppConfig = { endpoints: { [EModelEndpoint.agents]: { @@ -35,10 +35,9 @@ describe('primeResources', () => { }, } as AppConfig; - // Setup mock getFiles function mockGetFiles = jest.fn(); + mockFilterFiles = jest.fn().mockImplementation(({ files }) => Promise.resolve(files)); - // Setup request file set requestFileSet = new Set(['file1', 'file2', 'file3']); }); @@ -70,20 +69,21 @@ describe('primeResources', () => { req: mockReq, appConfig: mockAppConfig, getFiles: mockGetFiles, + filterFiles: mockFilterFiles, requestFileSet, attachments: undefined, tool_resources, + agentId: 'agent_test', }); - expect(mockGetFiles).toHaveBeenCalledWith( - { file_id: { $in: ['ocr-file-1'] } }, - {}, - {}, - { userId: undefined, agentId: undefined }, - ); + expect(mockGetFiles).toHaveBeenCalledWith({ file_id: { $in: ['ocr-file-1'] } }, {}, {}); + expect(mockFilterFiles).toHaveBeenCalledWith({ + files: mockOcrFiles, + userId: 'user1', + role: 'USER', + agentId: 'agent_test', + }); expect(result.attachments).toEqual(mockOcrFiles); - // Context field is deleted after files are fetched and re-categorized - // Since the file is not embedded and has no special properties, it won't be categorized expect(result.tool_resources).toEqual({}); }); }); @@ -1108,12 +1108,10 @@ describe('primeResources', () => { 'ocr-file-1', ); - // Verify getFiles was called with merged file_ids expect(mockGetFiles).toHaveBeenCalledWith( { file_id: { $in: ['context-file-1', 'ocr-file-1'] } }, {}, {}, - { userId: undefined, agentId: undefined }, ); }); @@ -1241,6 +1239,249 @@ describe('primeResources', () => { }); }); + describe('access control filtering', () => { + it('should filter context files through filterFiles when provided', async () => { + const ownedFile: TFile = { + user: 'user1', + file_id: 'owned-file', + filename: 'owned.pdf', + filepath: '/uploads/owned.pdf', + object: 'file', + type: 'application/pdf', + bytes: 1024, + embedded: false, + usage: 0, + }; + + const inaccessibleFile: TFile = { + user: 'other-user', + file_id: 'inaccessible-file', + filename: 'secret.pdf', + filepath: '/uploads/secret.pdf', + object: 'file', + type: 'application/pdf', + bytes: 2048, + embedded: false, + usage: 0, + }; + + mockGetFiles.mockResolvedValue([ownedFile, inaccessibleFile]); + mockFilterFiles.mockResolvedValue([ownedFile]); + + const tool_resources = { + [EToolResources.context]: { + file_ids: ['owned-file', 'inaccessible-file'], + }, + }; + + const result = await primeResources({ + req: mockReq, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + requestFileSet, + attachments: undefined, + tool_resources, + agentId: 'agent_shared', + }); + + expect(mockFilterFiles).toHaveBeenCalledWith({ + files: [ownedFile, inaccessibleFile], + userId: 'user1', + role: 'USER', + agentId: 'agent_shared', + }); + expect(result.attachments).toEqual([ownedFile]); + expect(result.attachments).not.toContainEqual(inaccessibleFile); + }); + + it('should filter OCR files merged into context through filterFiles', async () => { + const ocrFile: TFile = { + user: 'other-user', + file_id: 'ocr-restricted', + filename: 'scan.pdf', + filepath: '/uploads/scan.pdf', + object: 'file', + type: 'application/pdf', + bytes: 1024, + embedded: false, + usage: 0, + }; + + mockGetFiles.mockResolvedValue([ocrFile]); + mockFilterFiles.mockResolvedValue([]); + + const tool_resources = { + [EToolResources.ocr]: { + file_ids: ['ocr-restricted'], + }, + }; + + const result = await primeResources({ + req: mockReq, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + requestFileSet, + attachments: undefined, + tool_resources, + agentId: 'agent_shared', + }); + + expect(mockFilterFiles).toHaveBeenCalledWith({ + files: [ocrFile], + userId: 'user1', + role: 'USER', + agentId: 'agent_shared', + }); + expect(result.attachments).toBeUndefined(); + }); + + it('should skip filtering when filterFiles is not provided', async () => { + const mockFile: TFile = { + user: 'user1', + file_id: 'file-1', + filename: 'doc.pdf', + filepath: '/uploads/doc.pdf', + object: 'file', + type: 'application/pdf', + bytes: 1024, + embedded: false, + usage: 0, + }; + + mockGetFiles.mockResolvedValue([mockFile]); + + const tool_resources = { + [EToolResources.context]: { + file_ids: ['file-1'], + }, + }; + + const result = await primeResources({ + req: mockReq, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + requestFileSet, + attachments: undefined, + tool_resources, + agentId: 'agent_test', + }); + + expect(mockFilterFiles).not.toHaveBeenCalled(); + expect(result.attachments).toEqual([mockFile]); + }); + + it('should skip filtering when user ID is missing', async () => { + const reqNoUser = {} as unknown as ServerRequest & { user?: IUser }; + const mockFile: TFile = { + user: 'user1', + file_id: 'file-1', + filename: 'doc.pdf', + filepath: '/uploads/doc.pdf', + object: 'file', + type: 'application/pdf', + bytes: 1024, + embedded: false, + usage: 0, + }; + + mockGetFiles.mockResolvedValue([mockFile]); + + const tool_resources = { + [EToolResources.context]: { + file_ids: ['file-1'], + }, + }; + + const result = await primeResources({ + req: reqNoUser, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + requestFileSet, + attachments: undefined, + tool_resources, + agentId: 'agent_test', + }); + + expect(mockFilterFiles).not.toHaveBeenCalled(); + expect(result.attachments).toEqual([mockFile]); + }); + + it('should gracefully handle filterFiles rejection', async () => { + const mockFile: TFile = { + user: 'user1', + file_id: 'file-1', + filename: 'doc.pdf', + filepath: '/uploads/doc.pdf', + object: 'file', + type: 'application/pdf', + bytes: 1024, + embedded: false, + usage: 0, + }; + + mockGetFiles.mockResolvedValue([mockFile]); + mockFilterFiles.mockRejectedValue(new Error('DB failure')); + + const tool_resources = { + [EToolResources.context]: { + file_ids: ['file-1'], + }, + }; + + const result = await primeResources({ + req: mockReq, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + requestFileSet, + attachments: undefined, + tool_resources, + agentId: 'agent_test', + }); + + expect(logger.error).toHaveBeenCalledWith('Error priming resources', expect.any(Error)); + expect(result.tool_resources).toEqual(tool_resources); + }); + + it('should skip filtering when agentId is missing', async () => { + const mockFile: TFile = { + user: 'user1', + file_id: 'file-1', + filename: 'doc.pdf', + filepath: '/uploads/doc.pdf', + object: 'file', + type: 'application/pdf', + bytes: 1024, + embedded: false, + usage: 0, + }; + + mockGetFiles.mockResolvedValue([mockFile]); + + const tool_resources = { + [EToolResources.context]: { + file_ids: ['file-1'], + }, + }; + + const result = await primeResources({ + req: mockReq, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + requestFileSet, + attachments: undefined, + tool_resources, + }); + + expect(mockFilterFiles).not.toHaveBeenCalled(); + expect(result.attachments).toEqual([mockFile]); + }); + }); + describe('edge cases', () => { it('should handle missing appConfig agents endpoint gracefully', async () => { const reqWithoutLocals = {} as ServerRequest & { user?: IUser }; diff --git a/packages/api/src/agents/resources.ts b/packages/api/src/agents/resources.ts index 4655453847..e147c743cf 100644 --- a/packages/api/src/agents/resources.ts +++ b/packages/api/src/agents/resources.ts @@ -10,16 +10,26 @@ import type { Request as ServerRequest } from 'express'; * @param filter - MongoDB filter query for files * @param _sortOptions - Sorting options (currently unused) * @param selectFields - Field selection options - * @param options - Additional options including userId and agentId for access control * @returns Promise resolving to array of files */ export type TGetFiles = ( filter: FilterQuery, _sortOptions: ProjectionType | null | undefined, selectFields: QueryOptions | null | undefined, - options?: { userId?: string; agentId?: string }, ) => Promise>; +/** + * Function type for filtering files by agent access permissions. + * Used to enforce that only files the user has access to (via ownership or agent attachment) + * are returned after a raw DB query. + */ +export type TFilterFilesByAgentAccess = (params: { + files: Array; + userId: string; + role?: string; + agentId: string; +}) => Promise>; + /** * Helper function to add a file to a specific tool resource category * Prevents duplicate files within the same resource category @@ -128,7 +138,7 @@ const categorizeFileForToolResources = ({ /** * Primes resources for agent execution by processing attachments and tool resources * This function: - * 1. Fetches OCR files if OCR is enabled + * 1. Fetches context/OCR files (filtered by agent access control when available) * 2. Processes attachment files * 3. Categorizes files into appropriate tool resources * 4. Prevents duplicate files across all sources @@ -137,15 +147,18 @@ const categorizeFileForToolResources = ({ * @param params.req - Express request object * @param params.appConfig - Application configuration object * @param params.getFiles - Function to retrieve files from database + * @param params.filterFiles - Optional function to enforce agent-based file access control * @param params.requestFileSet - Set of file IDs from the current request * @param params.attachments - Promise resolving to array of attachment files * @param params.tool_resources - Existing tool resources for the agent + * @param params.agentId - Agent ID used for access control filtering * @returns Promise resolving to processed attachments and updated tool resources */ export const primeResources = async ({ req, appConfig, getFiles, + filterFiles, requestFileSet, attachments: _attachments, tool_resources: _tool_resources, @@ -157,6 +170,7 @@ export const primeResources = async ({ attachments: Promise> | undefined; tool_resources: AgentToolResources | undefined; getFiles: TGetFiles; + filterFiles?: TFilterFilesByAgentAccess; agentId?: string; }): Promise<{ attachments: Array | undefined; @@ -228,15 +242,23 @@ export const primeResources = async ({ if (fileIds.length > 0 && isContextEnabled) { delete tool_resources[EToolResources.context]; - const context = await getFiles( + let context = await getFiles( { file_id: { $in: fileIds }, }, {}, {}, - { userId: req.user?.id, agentId }, ); + if (filterFiles && req.user?.id && agentId) { + context = await filterFiles({ + files: context, + userId: req.user.id, + role: req.user.role, + agentId, + }); + } + for (const file of context) { if (!file?.file_id) { continue; From acd07e80852f6b931a4459372981b5d3db8082da Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 23:03:12 -0400 Subject: [PATCH 051/111] =?UTF-8?q?=F0=9F=97=9D=EF=B8=8F=20fix:=20Exempt?= =?UTF-8?q?=20Admin-Trusted=20Domains=20from=20MCP=20OAuth=20Validation=20?= =?UTF-8?q?(#12255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: exempt allowedDomains from MCP OAuth SSRF checks (#12254) The SSRF guard in validateOAuthUrl was context-blind — it blocked private/internal OAuth endpoints even for admin-trusted MCP servers listed in mcpSettings.allowedDomains. Add isHostnameAllowed() to domain.ts and skip SSRF checks in validateOAuthUrl when the OAuth endpoint hostname matches an allowed domain. * refactor: thread allowedDomains through MCP connection stack Pass allowedDomains from MCPServersRegistry through BasicConnectionOptions, MCPConnectionFactory, and into MCPOAuthHandler method calls so the OAuth layer can exempt admin-trusted domains from SSRF validation. * test: add allowedDomains bypass tests and fix registry mocks Add isHostnameAllowed unit tests (exact, wildcard, case-insensitive, private IPs). Add MCPOAuthSecurity tests covering the allowedDomains bypass for initiateOAuthFlow, refreshOAuthTokens, and revokeOAuthToken. Update registry mocks to include getAllowedDomains. * fix: enforce protocol/port constraints in OAuth allowedDomains bypass Replace isHostnameAllowed (hostname-only check) with isOAuthUrlAllowed which parses the full OAuth URL and matches against allowedDomains entries including protocol and explicit port constraints — mirroring isDomainAllowedCore's allowlist logic. Prevents a port-scoped entry like 'https://auth.internal:8443' from also exempting other ports. * test: cover auto-discovery and branch-3 refresh paths with allowedDomains Add three new integration tests using a real OAuth test server: - auto-discovered OAuth endpoints allowed when server IP is in allowedDomains - auto-discovered endpoints rejected when allowedDomains doesn't match - refreshOAuthTokens branch 3 (no clientInfo/config) with allowedDomains bypass Also rename describe block from ephemeral issue number to durable name. * docs: explain intentional absence of allowedDomains in completeOAuthFlow Prevents future contributors from assuming a missing parameter during security audits — URLs are pre-validated during initiateOAuthFlow. * test: update initiateOAuthFlow assertion for allowedDomains parameter * perf: avoid redundant URL parse for admin-trusted OAuth endpoints Move isOAuthUrlAllowed check before the hostname extraction so admin-trusted URLs short-circuit with a single URL parse instead of two. The hostname extraction (new URL) is now deferred to the SSRF-check path where it's actually needed. --- api/server/controllers/UserController.js | 3 + packages/api/src/auth/domain.spec.ts | 91 ++++++++ packages/api/src/auth/domain.ts | 46 ++++ packages/api/src/mcp/ConnectionsRepository.ts | 4 +- packages/api/src/mcp/MCPConnectionFactory.ts | 5 + packages/api/src/mcp/MCPManager.ts | 5 +- packages/api/src/mcp/UserConnectionManager.ts | 4 +- .../__tests__/ConnectionsRepository.test.ts | 4 + .../__tests__/MCPConnectionFactory.test.ts | 1 + .../api/src/mcp/__tests__/MCPManager.test.ts | 1 + .../__tests__/MCPOAuthRaceCondition.test.ts | 2 + .../mcp/__tests__/MCPOAuthSecurity.test.ts | 214 ++++++++++++++++++ packages/api/src/mcp/oauth/handler.ts | 59 +++-- .../src/mcp/registry/MCPServerInspector.ts | 10 +- packages/api/src/mcp/types/index.ts | 1 + 15 files changed, 432 insertions(+), 18 deletions(-) diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index b3160bb3d3..6d5df0ac8d 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -370,6 +370,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { serverConfig.oauth?.revocation_endpoint_auth_methods_supported ?? clientMetadata.revocation_endpoint_auth_methods_supported; const oauthHeaders = serverConfig.oauth_headers ?? {}; + const allowedDomains = getMCPServersRegistry().getAllowedDomains(); if (tokens?.access_token) { try { @@ -385,6 +386,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { revocationEndpointAuthMethodsSupported, }, oauthHeaders, + allowedDomains, ); } catch (error) { logger.error(`Error revoking OAuth access token for ${serverName}:`, error); @@ -405,6 +407,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { revocationEndpointAuthMethodsSupported, }, oauthHeaders, + allowedDomains, ); } catch (error) { logger.error(`Error revoking OAuth refresh token for ${serverName}:`, error); diff --git a/packages/api/src/auth/domain.spec.ts b/packages/api/src/auth/domain.spec.ts index a7140528a9..88a7c98160 100644 --- a/packages/api/src/auth/domain.spec.ts +++ b/packages/api/src/auth/domain.spec.ts @@ -8,6 +8,7 @@ import { extractMCPServerDomain, isActionDomainAllowed, isEmailDomainAllowed, + isOAuthUrlAllowed, isMCPDomainAllowed, isPrivateIP, isSSRFTarget, @@ -1211,6 +1212,96 @@ describe('isMCPDomainAllowed', () => { }); }); +describe('isOAuthUrlAllowed', () => { + it('should return false when allowedDomains is null/undefined/empty', () => { + expect(isOAuthUrlAllowed('https://example.com/token', null)).toBe(false); + expect(isOAuthUrlAllowed('https://example.com/token', undefined)).toBe(false); + expect(isOAuthUrlAllowed('https://example.com/token', [])).toBe(false); + }); + + it('should return false for unparseable URLs', () => { + expect(isOAuthUrlAllowed('not-a-url', ['example.com'])).toBe(false); + }); + + it('should match exact hostnames', () => { + expect(isOAuthUrlAllowed('https://example.com/token', ['example.com'])).toBe(true); + expect(isOAuthUrlAllowed('https://other.com/token', ['example.com'])).toBe(false); + }); + + it('should match wildcard subdomains', () => { + expect(isOAuthUrlAllowed('https://api.example.com/token', ['*.example.com'])).toBe(true); + expect(isOAuthUrlAllowed('https://deep.nested.example.com/token', ['*.example.com'])).toBe( + true, + ); + expect(isOAuthUrlAllowed('https://example.com/token', ['*.example.com'])).toBe(true); + expect(isOAuthUrlAllowed('https://other.com/token', ['*.example.com'])).toBe(false); + }); + + it('should be case-insensitive', () => { + expect(isOAuthUrlAllowed('https://EXAMPLE.COM/token', ['example.com'])).toBe(true); + expect(isOAuthUrlAllowed('https://example.com/token', ['EXAMPLE.COM'])).toBe(true); + }); + + it('should match private/internal URLs when hostname is in allowedDomains', () => { + expect(isOAuthUrlAllowed('http://localhost:8080/token', ['localhost'])).toBe(true); + expect(isOAuthUrlAllowed('http://10.0.0.1/token', ['10.0.0.1'])).toBe(true); + expect( + isOAuthUrlAllowed('http://host.docker.internal:8044/token', ['host.docker.internal']), + ).toBe(true); + expect(isOAuthUrlAllowed('http://myserver.local/token', ['*.local'])).toBe(true); + }); + + it('should match internal URLs with wildcard patterns', () => { + expect(isOAuthUrlAllowed('https://auth.company.internal/token', ['*.company.internal'])).toBe( + true, + ); + expect(isOAuthUrlAllowed('https://company.internal/token', ['*.company.internal'])).toBe(true); + }); + + it('should not match when hostname is absent from allowedDomains', () => { + expect(isOAuthUrlAllowed('http://10.0.0.1/token', ['192.168.1.1'])).toBe(false); + expect(isOAuthUrlAllowed('http://localhost/token', ['host.docker.internal'])).toBe(false); + }); + + describe('protocol and port constraint enforcement', () => { + it('should enforce protocol when allowedDomains specifies one', () => { + expect(isOAuthUrlAllowed('https://auth.internal/token', ['https://auth.internal'])).toBe( + true, + ); + expect(isOAuthUrlAllowed('http://auth.internal/token', ['https://auth.internal'])).toBe( + false, + ); + }); + + it('should allow any protocol when allowedDomains has bare hostname', () => { + expect(isOAuthUrlAllowed('http://auth.internal/token', ['auth.internal'])).toBe(true); + expect(isOAuthUrlAllowed('https://auth.internal/token', ['auth.internal'])).toBe(true); + }); + + it('should enforce port when allowedDomains specifies one', () => { + expect( + isOAuthUrlAllowed('https://auth.internal:8443/token', ['https://auth.internal:8443']), + ).toBe(true); + expect( + isOAuthUrlAllowed('https://auth.internal:6379/token', ['https://auth.internal:8443']), + ).toBe(false); + expect(isOAuthUrlAllowed('https://auth.internal/token', ['https://auth.internal:8443'])).toBe( + false, + ); + }); + + it('should allow any port when allowedDomains has no explicit port', () => { + expect(isOAuthUrlAllowed('https://auth.internal:8443/token', ['auth.internal'])).toBe(true); + expect(isOAuthUrlAllowed('https://auth.internal:22/token', ['auth.internal'])).toBe(true); + }); + + it('should reject wrong port even when hostname matches (prevents port-scanning)', () => { + expect(isOAuthUrlAllowed('http://10.0.0.1:6379/token', ['http://10.0.0.1:8080'])).toBe(false); + expect(isOAuthUrlAllowed('http://10.0.0.1:25/token', ['http://10.0.0.1:8080'])).toBe(false); + }); + }); +}); + describe('validateEndpointURL', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/packages/api/src/auth/domain.ts b/packages/api/src/auth/domain.ts index fabe2502ff..f4f9f5f04e 100644 --- a/packages/api/src/auth/domain.ts +++ b/packages/api/src/auth/domain.ts @@ -500,6 +500,52 @@ export async function isMCPDomainAllowed( return isDomainAllowedCore(domain, allowedDomains, MCP_PROTOCOLS); } +/** + * Checks whether an OAuth URL matches any entry in the MCP allowedDomains list, + * honoring protocol and port constraints when specified by the admin. + * + * Mirrors the allowlist-matching logic of {@link isDomainAllowedCore} (hostname, + * protocol, and explicit-port checks) but is synchronous — no DNS resolution is + * needed because the caller is deciding whether to *skip* the subsequent + * SSRF/DNS checks, not replace them. + * + * @remarks `parseDomainSpec` normalizes `www.` prefixes, so both the input URL + * and allowedDomains entries starting with `www.` are matched without that prefix. + */ +export function isOAuthUrlAllowed(url: string, allowedDomains?: string[] | null): boolean { + if (!Array.isArray(allowedDomains) || allowedDomains.length === 0) { + return false; + } + + const inputSpec = parseDomainSpec(url); + if (!inputSpec) { + return false; + } + + for (const allowedDomain of allowedDomains) { + const allowedSpec = parseDomainSpec(allowedDomain); + if (!allowedSpec) { + continue; + } + if (!hostnameMatches(inputSpec.hostname, allowedSpec)) { + continue; + } + if (allowedSpec.protocol !== null) { + if (inputSpec.protocol === null || inputSpec.protocol !== allowedSpec.protocol) { + continue; + } + } + if (allowedSpec.explicitPort) { + if (!inputSpec.explicitPort || inputSpec.port !== allowedSpec.port) { + continue; + } + } + return true; + } + + return false; +} + /** Matches ErrorTypes.INVALID_BASE_URL — string literal avoids build-time dependency on data-provider */ const INVALID_BASE_URL_TYPE = 'invalid_base_url'; diff --git a/packages/api/src/mcp/ConnectionsRepository.ts b/packages/api/src/mcp/ConnectionsRepository.ts index 970e7ea4b9..6313faa8d4 100644 --- a/packages/api/src/mcp/ConnectionsRepository.ts +++ b/packages/api/src/mcp/ConnectionsRepository.ts @@ -77,12 +77,14 @@ export class ConnectionsRepository { await this.disconnect(serverName); } } + const registry = MCPServersRegistry.getInstance(); const connection = await MCPConnectionFactory.create( { serverName, serverConfig, dbSourced: !!(serverConfig as t.ParsedServerConfig).dbId, - useSSRFProtection: MCPServersRegistry.getInstance().shouldEnableSSRFProtection(), + useSSRFProtection: registry.shouldEnableSSRFProtection(), + allowedDomains: registry.getAllowedDomains(), }, this.oauthOpts, ); diff --git a/packages/api/src/mcp/MCPConnectionFactory.ts b/packages/api/src/mcp/MCPConnectionFactory.ts index 0fc86e0315..b5b3d61bf0 100644 --- a/packages/api/src/mcp/MCPConnectionFactory.ts +++ b/packages/api/src/mcp/MCPConnectionFactory.ts @@ -30,6 +30,7 @@ export class MCPConnectionFactory { protected readonly logPrefix: string; protected readonly useOAuth: boolean; protected readonly useSSRFProtection: boolean; + protected readonly allowedDomains?: string[] | null; // OAuth-related properties (only set when useOAuth is true) protected readonly userId?: string; @@ -197,6 +198,7 @@ export class MCPConnectionFactory { this.serverName = basic.serverName; this.useOAuth = !!oauth?.useOAuth; this.useSSRFProtection = basic.useSSRFProtection === true; + this.allowedDomains = basic.allowedDomains; this.connectionTimeout = oauth?.connectionTimeout; this.logPrefix = oauth?.user ? `[MCP][${basic.serverName}][${oauth.user.id}]` @@ -297,6 +299,7 @@ export class MCPConnectionFactory { }, this.serverConfig.oauth_headers ?? {}, this.serverConfig.oauth, + this.allowedDomains, ); }; } @@ -340,6 +343,7 @@ export class MCPConnectionFactory { this.userId!, config?.oauth_headers ?? {}, config?.oauth, + this.allowedDomains, ); if (existingFlow) { @@ -603,6 +607,7 @@ export class MCPConnectionFactory { this.userId!, this.serverConfig.oauth_headers ?? {}, this.serverConfig.oauth, + this.allowedDomains, ); // Store flow state BEFORE redirecting so the callback can find it diff --git a/packages/api/src/mcp/MCPManager.ts b/packages/api/src/mcp/MCPManager.ts index 6fdf45c27a..afb6c68796 100644 --- a/packages/api/src/mcp/MCPManager.ts +++ b/packages/api/src/mcp/MCPManager.ts @@ -100,13 +100,16 @@ export class MCPManager extends UserConnectionManager { const useOAuth = Boolean(serverConfig.requiresOAuth || serverConfig.oauthMetadata); - const useSSRFProtection = MCPServersRegistry.getInstance().shouldEnableSSRFProtection(); + const registry = MCPServersRegistry.getInstance(); + const useSSRFProtection = registry.shouldEnableSSRFProtection(); + const allowedDomains = registry.getAllowedDomains(); const dbSourced = !!serverConfig.dbId; const basic: t.BasicConnectionOptions = { dbSourced, serverName, serverConfig, useSSRFProtection, + allowedDomains, }; if (!useOAuth) { diff --git a/packages/api/src/mcp/UserConnectionManager.ts b/packages/api/src/mcp/UserConnectionManager.ts index 76523fc0fc..2e9d5be467 100644 --- a/packages/api/src/mcp/UserConnectionManager.ts +++ b/packages/api/src/mcp/UserConnectionManager.ts @@ -153,12 +153,14 @@ export abstract class UserConnectionManager { logger.info(`[MCP][User: ${userId}][${serverName}] Establishing new connection`); try { + const registry = MCPServersRegistry.getInstance(); connection = await MCPConnectionFactory.create( { serverConfig: config, serverName: serverName, dbSourced: !!config.dbId, - useSSRFProtection: MCPServersRegistry.getInstance().shouldEnableSSRFProtection(), + useSSRFProtection: registry.shouldEnableSSRFProtection(), + allowedDomains: registry.getAllowedDomains(), }, { useOAuth: true, diff --git a/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts b/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts index 98e15eca18..7a93960765 100644 --- a/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts +++ b/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts @@ -25,6 +25,7 @@ const mockRegistryInstance = { getServerConfig: jest.fn(), getAllServerConfigs: jest.fn(), shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + getAllowedDomains: jest.fn().mockReturnValue(null), }; jest.mock('../registry/MCPServersRegistry', () => ({ @@ -110,6 +111,7 @@ describe('ConnectionsRepository', () => { serverName: 'server1', serverConfig: mockServerConfigs.server1, useSSRFProtection: false, + allowedDomains: null, dbSourced: false, }, undefined, @@ -133,6 +135,7 @@ describe('ConnectionsRepository', () => { serverName: 'server1', serverConfig: mockServerConfigs.server1, useSSRFProtection: false, + allowedDomains: null, dbSourced: false, }, undefined, @@ -173,6 +176,7 @@ describe('ConnectionsRepository', () => { serverName: 'server1', serverConfig: configWithCachedAt, useSSRFProtection: false, + allowedDomains: null, dbSourced: false, }, undefined, diff --git a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts index bceb23b246..23bfa89d56 100644 --- a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts +++ b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts @@ -269,6 +269,7 @@ describe('MCPConnectionFactory', () => { 'user123', {}, undefined, + undefined, ); // initFlow must be awaited BEFORE the redirect to guarantee state is stored diff --git a/packages/api/src/mcp/__tests__/MCPManager.test.ts b/packages/api/src/mcp/__tests__/MCPManager.test.ts index bf63a6af3c..dd1ead0dd9 100644 --- a/packages/api/src/mcp/__tests__/MCPManager.test.ts +++ b/packages/api/src/mcp/__tests__/MCPManager.test.ts @@ -34,6 +34,7 @@ const mockRegistryInstance = { getAllServerConfigs: jest.fn(), getOAuthServers: jest.fn(), shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + getAllowedDomains: jest.fn().mockReturnValue(null), }; jest.mock('~/mcp/registry/MCPServersRegistry', () => ({ diff --git a/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts index 85febb3ece..cb6187ab45 100644 --- a/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts +++ b/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts @@ -82,6 +82,7 @@ describe('MCP OAuth Race Condition Fixes', () => { .mockReturnValue({ getServerConfig: jest.fn().mockResolvedValue(mockConfig), shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + getAllowedDomains: jest.fn().mockReturnValue(null), }); const { MCPConnectionFactory } = await import('~/mcp/MCPConnectionFactory'); @@ -147,6 +148,7 @@ describe('MCP OAuth Race Condition Fixes', () => { .mockReturnValue({ getServerConfig: jest.fn().mockResolvedValue(mockConfig), shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + getAllowedDomains: jest.fn().mockReturnValue(null), }); const { MCPConnectionFactory } = await import('~/mcp/MCPConnectionFactory'); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts index a5188e24b0..a2d0440d42 100644 --- a/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts +++ b/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts @@ -7,6 +7,9 @@ * * 2. redirect_uri manipulation — validates that user-supplied redirect_uri * is ignored in favor of the server-controlled default. + * + * 3. allowedDomains SSRF exemption — validates that admin-configured allowedDomains + * exempts trusted domains from SSRF checks, including auto-discovery paths. */ import * as http from 'http'; @@ -226,3 +229,214 @@ describe('MCP OAuth redirect_uri enforcement', () => { expect(authUrl.searchParams.get('redirect_uri')).not.toBe(attackerRedirectUri); }); }); + +describe('MCP OAuth allowedDomains SSRF exemption for admin-trusted hosts', () => { + it('should allow private authorization_url when hostname is in allowedDomains', async () => { + const result = await MCPOAuthHandler.initiateOAuthFlow( + 'internal-server', + 'https://speedy-mcp.company.com/', + 'user-1', + {}, + { + authorization_url: 'http://10.0.0.1/authorize', + token_url: 'http://10.0.0.1/token', + client_id: 'client', + client_secret: 'secret', + }, + ['10.0.0.1'], + ); + + expect(result.authorizationUrl).toContain('10.0.0.1/authorize'); + }); + + it('should allow private token_url when hostname matches wildcard allowedDomains', async () => { + const result = await MCPOAuthHandler.initiateOAuthFlow( + 'internal-server', + 'https://speedy-mcp.company.com/', + 'user-1', + {}, + { + authorization_url: 'https://auth.company.internal/authorize', + token_url: 'https://auth.company.internal/token', + client_id: 'client', + client_secret: 'secret', + }, + ['*.company.internal'], + ); + + expect(result.authorizationUrl).toContain('auth.company.internal/authorize'); + }); + + it('should still reject private URLs when allowedDomains does not match', async () => { + await expect( + MCPOAuthHandler.initiateOAuthFlow( + 'test-server', + 'https://mcp.example.com/', + 'user-1', + {}, + { + authorization_url: 'http://169.254.169.254/authorize', + token_url: 'https://auth.example.com/token', + client_id: 'client', + client_secret: 'secret', + }, + ['safe.example.com'], + ), + ).rejects.toThrow(/targets a blocked address/); + }); + + it('should still reject when allowedDomains is empty', async () => { + await expect( + MCPOAuthHandler.initiateOAuthFlow( + 'test-server', + 'https://mcp.example.com/', + 'user-1', + {}, + { + authorization_url: 'http://10.0.0.1/authorize', + token_url: 'https://auth.example.com/token', + client_id: 'client', + client_secret: 'secret', + }, + [], + ), + ).rejects.toThrow(/targets a blocked address/); + }); + + it('should allow private revocationEndpoint when hostname is in allowedDomains', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + } as Response); + const originalFetch = global.fetch; + global.fetch = mockFetch; + + try { + await MCPOAuthHandler.revokeOAuthToken( + 'internal-server', + 'some-token', + 'access', + { + serverUrl: 'https://internal.corp.net/', + clientId: 'client', + clientSecret: 'secret', + revocationEndpoint: 'http://10.0.0.1/revoke', + }, + {}, + ['10.0.0.1'], + ); + + expect(mockFetch).toHaveBeenCalled(); + } finally { + global.fetch = originalFetch; + } + }); + + it('should allow localhost token_url in refreshOAuthTokens when localhost is in allowedDomains', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + token_type: 'Bearer', + expires_in: 3600, + }), + } as Response); + const originalFetch = global.fetch; + global.fetch = mockFetch; + + try { + const tokens = await MCPOAuthHandler.refreshOAuthTokens( + 'old-refresh-token', + { + serverName: 'local-server', + serverUrl: 'http://localhost:8080/', + clientInfo: { + client_id: 'client-id', + client_secret: 'client-secret', + redirect_uris: ['http://localhost:3080/callback'], + }, + }, + {}, + { + token_url: 'http://localhost:8080/token', + client_id: 'client-id', + client_secret: 'client-secret', + }, + ['localhost'], + ); + + expect(tokens.access_token).toBe('new-access-token'); + expect(mockFetch).toHaveBeenCalled(); + } finally { + global.fetch = originalFetch; + } + }); + + describe('auto-discovery path with allowedDomains', () => { + let discoveryServer: OAuthTestServer; + + beforeEach(async () => { + discoveryServer = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + }); + + afterEach(async () => { + await discoveryServer.close(); + }); + + it('should allow auto-discovered OAuth endpoints when server IP is in allowedDomains', async () => { + const result = await MCPOAuthHandler.initiateOAuthFlow( + 'discovery-server', + discoveryServer.url, + 'user-1', + {}, + undefined, + ['127.0.0.1'], + ); + + expect(result.authorizationUrl).toContain('127.0.0.1'); + expect(result.flowId).toBeTruthy(); + }); + + it('should reject auto-discovered endpoints when allowedDomains does not cover server IP', async () => { + await expect( + MCPOAuthHandler.initiateOAuthFlow( + 'discovery-server', + discoveryServer.url, + 'user-1', + {}, + undefined, + ['safe.example.com'], + ), + ).rejects.toThrow(/targets a blocked address/); + }); + + it('should allow auto-discovered token_url in refreshOAuthTokens branch 3 (no clientInfo/config)', async () => { + const code = await discoveryServer.getAuthCode(); + const tokenRes = await fetch(`${discoveryServer.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + refresh_token: string; + }; + + const tokens = await MCPOAuthHandler.refreshOAuthTokens( + initial.refresh_token, + { + serverName: 'discovery-refresh-server', + serverUrl: discoveryServer.url, + }, + {}, + undefined, + ['127.0.0.1'], + ); + + expect(tokens.access_token).toBeTruthy(); + }); + }); +}); diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 8d863bfe79..0a9154ff35 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -24,7 +24,7 @@ import { selectRegistrationAuthMethod, inferClientAuthMethod, } from './methods'; -import { isSSRFTarget, resolveHostnameSSRF } from '~/auth'; +import { isSSRFTarget, resolveHostnameSSRF, isOAuthUrlAllowed } from '~/auth'; import { sanitizeUrlForLogging } from '~/mcp/utils'; /** Type for the OAuth metadata from the SDK */ @@ -123,6 +123,7 @@ export class MCPOAuthHandler { private static async discoverMetadata( serverUrl: string, oauthHeaders: Record, + allowedDomains?: string[] | null, ): Promise<{ metadata: OAuthMetadata; resourceMetadata?: OAuthProtectedResourceMetadata; @@ -146,7 +147,7 @@ export class MCPOAuthHandler { if (resourceMetadata?.authorization_servers?.length) { const discoveredAuthServer = resourceMetadata.authorization_servers[0]; - await this.validateOAuthUrl(discoveredAuthServer, 'authorization_server'); + await this.validateOAuthUrl(discoveredAuthServer, 'authorization_server', allowedDomains); authServerUrl = new URL(discoveredAuthServer); logger.debug( `[MCPOAuth] Found authorization server from resource metadata: ${authServerUrl}`, @@ -206,11 +207,17 @@ export class MCPOAuthHandler { const endpointChecks: Promise[] = []; if (metadata.registration_endpoint) { endpointChecks.push( - this.validateOAuthUrl(metadata.registration_endpoint, 'registration_endpoint'), + this.validateOAuthUrl( + metadata.registration_endpoint, + 'registration_endpoint', + allowedDomains, + ), ); } if (metadata.token_endpoint) { - endpointChecks.push(this.validateOAuthUrl(metadata.token_endpoint, 'token_endpoint')); + endpointChecks.push( + this.validateOAuthUrl(metadata.token_endpoint, 'token_endpoint', allowedDomains), + ); } if (endpointChecks.length > 0) { await Promise.all(endpointChecks); @@ -360,6 +367,7 @@ export class MCPOAuthHandler { userId: string, oauthHeaders: Record, config?: MCPOptions['oauth'], + allowedDomains?: string[] | null, ): Promise<{ authorizationUrl: string; flowId: string; flowMetadata: MCPOAuthFlowMetadata }> { logger.debug( `[MCPOAuth] initiateOAuthFlow called for ${serverName} with URL: ${sanitizeUrlForLogging(serverUrl)}`, @@ -375,8 +383,8 @@ export class MCPOAuthHandler { logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for ${serverName}`); await Promise.all([ - this.validateOAuthUrl(config.authorization_url, 'authorization_url'), - this.validateOAuthUrl(config.token_url, 'token_url'), + this.validateOAuthUrl(config.authorization_url, 'authorization_url', allowedDomains), + this.validateOAuthUrl(config.token_url, 'token_url', allowedDomains), ]); const skipCodeChallengeCheck = @@ -477,6 +485,7 @@ export class MCPOAuthHandler { const { metadata, resourceMetadata, authServerUrl } = await this.discoverMetadata( serverUrl, oauthHeaders, + allowedDomains, ); logger.debug( @@ -588,7 +597,11 @@ export class MCPOAuthHandler { } /** - * Completes the OAuth flow by exchanging the authorization code for tokens + * Completes the OAuth flow by exchanging the authorization code for tokens. + * + * `allowedDomains` is intentionally absent: all URLs used here (serverUrl, + * token_endpoint) originate from {@link MCPOAuthFlowMetadata} that was + * SSRF-validated during {@link initiateOAuthFlow}. No new URL resolution occurs. */ static async completeOAuthFlow( flowId: string, @@ -692,8 +705,20 @@ export class MCPOAuthHandler { return randomBytes(32).toString('base64url'); } - /** Validates an OAuth URL is not targeting a private/internal address */ - private static async validateOAuthUrl(url: string, fieldName: string): Promise { + /** + * Validates an OAuth URL is not targeting a private/internal address. + * Skipped when the full URL (hostname + protocol + port) matches an admin-trusted + * allowedDomains entry, honoring protocol/port constraints when the admin specifies them. + */ + private static async validateOAuthUrl( + url: string, + fieldName: string, + allowedDomains?: string[] | null, + ): Promise { + if (isOAuthUrlAllowed(url, allowedDomains)) { + return; + } + let hostname: string; try { hostname = new URL(url).hostname; @@ -799,6 +824,7 @@ export class MCPOAuthHandler { metadata: { serverName: string; serverUrl?: string; clientInfo?: OAuthClientInformation }, oauthHeaders: Record, config?: MCPOptions['oauth'], + allowedDomains?: string[] | null, ): Promise { logger.debug(`[MCPOAuth] Refreshing tokens for ${metadata.serverName}`); @@ -824,7 +850,7 @@ export class MCPOAuthHandler { let tokenUrl: string; let authMethods: string[] | undefined; if (config?.token_url) { - await this.validateOAuthUrl(config.token_url, 'token_url'); + await this.validateOAuthUrl(config.token_url, 'token_url', allowedDomains); tokenUrl = config.token_url; authMethods = config.token_endpoint_auth_methods_supported; } else if (!metadata.serverUrl) { @@ -851,7 +877,7 @@ export class MCPOAuthHandler { tokenUrl = oauthMetadata.token_endpoint; authMethods = oauthMetadata.token_endpoint_auth_methods_supported; } - await this.validateOAuthUrl(tokenUrl, 'token_url'); + await this.validateOAuthUrl(tokenUrl, 'token_url', allowedDomains); } const body = new URLSearchParams({ @@ -928,7 +954,7 @@ export class MCPOAuthHandler { if (config?.token_url && config?.client_id) { logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for token refresh`); - await this.validateOAuthUrl(config.token_url, 'token_url'); + await this.validateOAuthUrl(config.token_url, 'token_url', allowedDomains); const tokenUrl = new URL(config.token_url); const body = new URLSearchParams({ @@ -1026,7 +1052,7 @@ export class MCPOAuthHandler { } else { tokenUrl = new URL(oauthMetadata.token_endpoint); } - await this.validateOAuthUrl(tokenUrl.href, 'token_url'); + await this.validateOAuthUrl(tokenUrl.href, 'token_url', allowedDomains); const body = new URLSearchParams({ grant_type: 'refresh_token', @@ -1075,9 +1101,14 @@ export class MCPOAuthHandler { revocationEndpointAuthMethodsSupported?: string[]; }, oauthHeaders: Record = {}, + allowedDomains?: string[] | null, ): Promise { if (metadata.revocationEndpoint != null) { - await this.validateOAuthUrl(metadata.revocationEndpoint, 'revocation_endpoint'); + await this.validateOAuthUrl( + metadata.revocationEndpoint, + 'revocation_endpoint', + allowedDomains, + ); } const revokeUrl: URL = metadata.revocationEndpoint != null diff --git a/packages/api/src/mcp/registry/MCPServerInspector.ts b/packages/api/src/mcp/registry/MCPServerInspector.ts index a477d9b412..7f31211680 100644 --- a/packages/api/src/mcp/registry/MCPServerInspector.ts +++ b/packages/api/src/mcp/registry/MCPServerInspector.ts @@ -20,6 +20,7 @@ export class MCPServerInspector { private readonly config: t.ParsedServerConfig, private connection: MCPConnection | undefined, private readonly useSSRFProtection: boolean = false, + private readonly allowedDomains?: string[] | null, ) {} /** @@ -46,7 +47,13 @@ export class MCPServerInspector { const useSSRFProtection = !Array.isArray(allowedDomains) || allowedDomains.length === 0; const start = Date.now(); - const inspector = new MCPServerInspector(serverName, rawConfig, connection, useSSRFProtection); + const inspector = new MCPServerInspector( + serverName, + rawConfig, + connection, + useSSRFProtection, + allowedDomains, + ); await inspector.inspectServer(); inspector.config.initDuration = Date.now() - start; return inspector.config; @@ -68,6 +75,7 @@ export class MCPServerInspector { serverName: this.serverName, dbSourced: !!this.config.dbId, useSSRFProtection: this.useSSRFProtection, + allowedDomains: this.allowedDomains, }); } diff --git a/packages/api/src/mcp/types/index.ts b/packages/api/src/mcp/types/index.ts index bbdabb4428..0af10c7399 100644 --- a/packages/api/src/mcp/types/index.ts +++ b/packages/api/src/mcp/types/index.ts @@ -169,6 +169,7 @@ export interface BasicConnectionOptions { serverName: string; serverConfig: MCPOptions; useSSRFProtection?: boolean; + allowedDomains?: string[] | null; /** When true, only resolve customUserVars in processMCPEnv (for DB-stored servers) */ dbSourced?: boolean; } From 8271055c2da8c0eb18d6cb7703525e11289bef59 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 15 Mar 2026 23:51:41 -0400 Subject: [PATCH 052/111] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20`@librec?= =?UTF-8?q?hat/agents`=20to=20v3.1.56=20(#12258)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦 chore: Bump `@librechat/agents` to v3.1.56 * chore: resolve type error, URL property check in isMCPDomainAllowed function --- api/package.json | 2 +- package-lock.json | 11 ++++++----- packages/api/package.json | 2 +- packages/api/src/auth/domain.ts | 4 +++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/api/package.json b/api/package.json index 0305446818..89a5183ddd 100644 --- a/api/package.json +++ b/api/package.json @@ -44,7 +44,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.55", + "@librechat/agents": "^3.1.56", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index 502b3a8eed..45f737ad8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.55", + "@librechat/agents": "^3.1.56", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -12324,9 +12324,9 @@ } }, "node_modules/@librechat/agents": { - "version": "3.1.55", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.55.tgz", - "integrity": "sha512-impxeKpCDlPkAVQFWnA6u6xkxDSBR/+H8uYq7rZomBeu0rUh/OhJLiI1fAwPhKXP33udNtHA8GyDi0QJj78R9w==", + "version": "3.1.56", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.56.tgz", + "integrity": "sha512-HJJwRnLM4XKpTWB4/wPDJR+iegyKBVUwqj7A8QHqzEcHzjKJDTr3wBPxZVH1tagGr6/mbbnErOJ14cH1OSNmpA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -12347,6 +12347,7 @@ "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "@scarf/scarf": "^1.4.0", + "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", @@ -44239,7 +44240,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.55", + "@librechat/agents": "^3.1.56", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/package.json b/packages/api/package.json index 77258fc0b3..b3b40c79a2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -90,7 +90,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.55", + "@librechat/agents": "^3.1.56", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/src/auth/domain.ts b/packages/api/src/auth/domain.ts index f4f9f5f04e..f5719829d5 100644 --- a/packages/api/src/auth/domain.ts +++ b/packages/api/src/auth/domain.ts @@ -485,7 +485,9 @@ export async function isMCPDomainAllowed( const hasAllowlist = Array.isArray(allowedDomains) && allowedDomains.length > 0; const hasExplicitUrl = - Object.hasOwn(config, 'url') && typeof config.url === 'string' && config.url.trim().length > 0; + Object.prototype.hasOwnProperty.call(config, 'url') && + typeof config.url === 'string' && + config.url.trim().length > 0; if (!domain && hasExplicitUrl && hasAllowlist) { return false; From 85e24e4c61156a49440e25b5c93689b3da8a743e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:18:07 -0400 Subject: [PATCH 053/111] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#12259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/en/translation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 36d882c6a2..afd1072b61 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -368,11 +368,11 @@ "com_error_illegal_model_request": "The model \"{{0}}\" is not available for {{1}}. Please select a different model.", "com_error_input_length": "The latest message token count is too long, exceeding the token limit, or your token limit parameters are misconfigured, adversely affecting the context window. More info: {{0}}. Please shorten your message, adjust the max context size from the conversation parameters, or fork the conversation to continue.", "com_error_invalid_agent_provider": "The \"{{0}}\" provider is not available for use with Agents. Please go to your agent's settings and select a currently available provider.", + "com_error_invalid_base_url": "The base URL you provided targets a restricted address. Please use a valid external URL and try again.", "com_error_invalid_user_key": "Invalid key provided. Please provide a valid key and try again.", "com_error_missing_model": "No model selected for {{0}}. Please select a model and try again.", "com_error_models_not_loaded": "Models configuration could not be loaded. Please refresh the page and try again.", "com_error_moderation": "It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We're unable to proceed with this specific topic. If you have any other questions or topics you'd like to explore, please edit your message, or create a new conversation.", - "com_error_invalid_base_url": "The base URL you provided targets a restricted address. Please use a valid external URL and try again.", "com_error_no_base_url": "No base URL found. Please provide one and try again.", "com_error_no_user_key": "No key found. Please provide a key and try again.", "com_error_refusal": "Response refused by safety filters. Rewrite your message and try again. If you encounter this frequently while using Claude Sonnet 4.5 or Opus 4.1, you can try Sonnet 4, which has different usage restrictions.", @@ -748,8 +748,8 @@ "com_ui_at_least_one_owner_required": "At least one owner is required", "com_ui_attach_error": "Cannot attach file. Create or select a conversation, or try refreshing the page.", "com_ui_attach_error_disabled": "File uploads are disabled for this endpoint", - "com_ui_attach_error_openai": "Cannot attach Assistant files to other endpoints", "com_ui_attach_error_limit": "File limit reached:", + "com_ui_attach_error_openai": "Cannot attach Assistant files to other endpoints", "com_ui_attach_error_size": "File size limit exceeded for endpoint:", "com_ui_attach_error_total_size": "Total file size limit exceeded for endpoint:", "com_ui_attach_error_type": "Unsupported file type for endpoint:", From 951d261f5c19d488b245a977d37569a5b069a45e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 16 Mar 2026 08:48:24 -0400 Subject: [PATCH 054/111] =?UTF-8?q?=F0=9F=A7=AF=20fix:=20Prevent=20Env-Var?= =?UTF-8?q?iable=20Exfil.=20via=20Placeholder=20Injection=20(#12260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 fix: Resolve env vars before body placeholder expansion to prevent secret exfiltration Body placeholders ({{LIBRECHAT_BODY_*}}) were substituted before extractEnvVariable ran, allowing user-controlled body fields containing ${SECRET} patterns to be expanded into real environment values in outbound headers. Reorder so env vars resolve first, preventing untrusted input from triggering env expansion. * 🛡️ fix: Block sensitive infrastructure env vars from placeholder resolution Add isSensitiveEnvVar blocklist to extractEnvVariable so that internal infrastructure secrets (JWT_SECRET, JWT_REFRESH_SECRET, CREDS_KEY, CREDS_IV, MEILI_MASTER_KEY, MONGO_URI, REDIS_URI, REDIS_PASSWORD) can never be resolved via ${VAR} expansion — even if an attacker manages to inject a placeholder pattern. Uses exact-match set (not substring patterns) to avoid breaking legitimate operator config that references OAuth/API secrets in MCP and custom endpoint configurations. * 🧹 test: Rename ANOTHER_SECRET test fixture to ANOTHER_VALUE Avoid using SECRET-containing names for non-sensitive test fixtures to prevent confusion with the new isSensitiveEnvVar blocklist. * 🔒 fix: Resolve env vars before all user-controlled substitutions in processSingleValue Move extractEnvVariable to run on the raw admin-authored template BEFORE customUserVars, user fields, OIDC tokens, and body placeholders. Previously env resolution ran after customUserVars, so a user setting a custom MCP variable to "${SECRET}" could still trigger env expansion. Now env vars are resolved strictly on operator config, and all subsequent user-controlled substitutions cannot introduce ${VAR} patterns that would be expanded. Gated by !dbSourced so DB-stored servers continue to skip env resolution. Adds a security-invariant comment documenting the ordering requirement. * 🧪 test: Comprehensive security regression tests for placeholder injection - Cover all three body fields (conversationId, parentMessageId, messageId) - Add user-field injection test (user.name containing ${VAR}) - Add customUserVars injection test (MY_TOKEN = "${VAR}") - Add processMCPEnv injection tests for body and customUserVars paths - Remove redundant process.env setup/teardown already handled by beforeEach/afterEach * 🧹 chore: Add REDIS_PASSWORD to blocklist integration test; document customUserVars gate --- packages/api/src/utils/env.spec.ts | 97 +++++++++++++++++++--- packages/api/src/utils/env.ts | 15 +++- packages/data-provider/specs/utils.spec.ts | 75 ++++++++++++++++- packages/data-provider/src/utils.ts | 37 +++++++-- 4 files changed, 200 insertions(+), 24 deletions(-) diff --git a/packages/api/src/utils/env.spec.ts b/packages/api/src/utils/env.spec.ts index c241cb2b51..e1244fa605 100644 --- a/packages/api/src/utils/env.spec.ts +++ b/packages/api/src/utils/env.spec.ts @@ -111,12 +111,12 @@ describe('encodeHeaderValue', () => { describe('resolveHeaders', () => { beforeEach(() => { process.env.TEST_API_KEY = 'test-api-key-value'; - process.env.ANOTHER_SECRET = 'another-secret-value'; + process.env.ANOTHER_VALUE = 'another-test-value'; }); afterEach(() => { delete process.env.TEST_API_KEY; - delete process.env.ANOTHER_SECRET; + delete process.env.ANOTHER_VALUE; }); it('should return empty object when headers is undefined', () => { @@ -139,7 +139,7 @@ describe('resolveHeaders', () => { it('should process environment variables in headers', () => { const headers = { Authorization: '${TEST_API_KEY}', - 'X-Secret': '${ANOTHER_SECRET}', + 'X-Secret': '${ANOTHER_VALUE}', 'Content-Type': 'application/json', }; @@ -147,7 +147,7 @@ describe('resolveHeaders', () => { expect(result).toEqual({ Authorization: 'test-api-key-value', - 'X-Secret': 'another-secret-value', + 'X-Secret': 'another-test-value', 'Content-Type': 'application/json', }); }); @@ -526,6 +526,40 @@ describe('resolveHeaders', () => { expect(result['X-Conversation']).toBe('conv-123'); }); + it('should not resolve env vars introduced via LIBRECHAT_BODY placeholders', () => { + const body = { + conversationId: '${TEST_API_KEY}', + parentMessageId: '${TEST_API_KEY}', + messageId: '${TEST_API_KEY}', + }; + const headers = { + 'X-Conv': '{{LIBRECHAT_BODY_CONVERSATIONID}}', + 'X-Parent': '{{LIBRECHAT_BODY_PARENTMESSAGEID}}', + 'X-Msg': '{{LIBRECHAT_BODY_MESSAGEID}}', + }; + const result = resolveHeaders({ headers, body }); + + expect(result['X-Conv']).toBe('${TEST_API_KEY}'); + expect(result['X-Parent']).toBe('${TEST_API_KEY}'); + expect(result['X-Msg']).toBe('${TEST_API_KEY}'); + }); + + it('should not resolve env vars introduced via LIBRECHAT_USER placeholders', () => { + const user = createTestUser({ name: '${TEST_API_KEY}' }); + const headers = { 'X-Name': '{{LIBRECHAT_USER_NAME}}' }; + const result = resolveHeaders({ headers, user }); + + expect(result['X-Name']).toBe('${TEST_API_KEY}'); + }); + + it('should not resolve env vars introduced via customUserVars', () => { + const customUserVars = { MY_TOKEN: '${TEST_API_KEY}' }; + const headers = { Authorization: 'Bearer {{MY_TOKEN}}' }; + const result = resolveHeaders({ headers, customUserVars }); + + expect(result.Authorization).toBe('Bearer ${TEST_API_KEY}'); + }); + describe('non-string header values (type guard tests)', () => { it('should handle numeric header values without crashing', () => { const headers = { @@ -657,12 +691,12 @@ describe('resolveHeaders', () => { describe('resolveNestedObject', () => { beforeEach(() => { process.env.TEST_API_KEY = 'test-api-key-value'; - process.env.ANOTHER_SECRET = 'another-secret-value'; + process.env.ANOTHER_VALUE = 'another-test-value'; }); afterEach(() => { delete process.env.TEST_API_KEY; - delete process.env.ANOTHER_SECRET; + delete process.env.ANOTHER_VALUE; }); it('should preserve nested object structure', () => { @@ -952,7 +986,7 @@ describe('resolveNestedObject', () => { describe('processMCPEnv', () => { beforeEach(() => { process.env.TEST_API_KEY = 'test-api-key-value'; - process.env.ANOTHER_SECRET = 'another-secret-value'; + process.env.ANOTHER_VALUE = 'another-test-value'; process.env.OAUTH_CLIENT_ID = 'oauth-client-id-value'; process.env.OAUTH_CLIENT_SECRET = 'oauth-client-secret-value'; process.env.MCP_SERVER_URL = 'https://mcp.example.com'; @@ -960,7 +994,7 @@ describe('processMCPEnv', () => { afterEach(() => { delete process.env.TEST_API_KEY; - delete process.env.ANOTHER_SECRET; + delete process.env.ANOTHER_VALUE; delete process.env.OAUTH_CLIENT_ID; delete process.env.OAUTH_CLIENT_SECRET; delete process.env.MCP_SERVER_URL; @@ -977,7 +1011,7 @@ describe('processMCPEnv', () => { command: 'mcp-server', env: { API_KEY: '${TEST_API_KEY}', - SECRET: '${ANOTHER_SECRET}', + SECRET: '${ANOTHER_VALUE}', PLAIN_VALUE: 'plain-text', }, args: ['--key', '${TEST_API_KEY}', '--url', '${MCP_SERVER_URL}'], @@ -990,7 +1024,7 @@ describe('processMCPEnv', () => { command: 'mcp-server', env: { API_KEY: 'test-api-key-value', - SECRET: 'another-secret-value', + SECRET: 'another-test-value', PLAIN_VALUE: 'plain-text', }, args: ['--key', 'test-api-key-value', '--url', 'https://mcp.example.com'], @@ -1137,6 +1171,49 @@ describe('processMCPEnv', () => { }); }); + it('should not resolve env vars introduced via body placeholders in MCP headers', () => { + const body = { + conversationId: '${TEST_API_KEY}', + parentMessageId: '${TEST_API_KEY}', + messageId: '${TEST_API_KEY}', + }; + + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + 'X-Conv': '{{LIBRECHAT_BODY_CONVERSATIONID}}', + 'X-Parent': '{{LIBRECHAT_BODY_PARENTMESSAGEID}}', + }, + }; + + const result = processMCPEnv({ options, body }); + + if (!isStreamableHTTPOptions(result)) { + throw new Error('Expected streamable-http options'); + } + expect(result.headers?.['X-Conv']).toBe('${TEST_API_KEY}'); + expect(result.headers?.['X-Parent']).toBe('${TEST_API_KEY}'); + }); + + it('should not resolve env vars introduced via customUserVars in MCP headers', () => { + const customUserVars = { MY_TOKEN: '${TEST_API_KEY}' }; + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer {{MY_TOKEN}}', + }, + }; + + const result = processMCPEnv({ options, customUserVars }); + + if (!isStreamableHTTPOptions(result)) { + throw new Error('Expected streamable-http options'); + } + expect(result.headers?.Authorization).toBe('Bearer ${TEST_API_KEY}'); + }); + it('should handle mixed placeholders in OAuth configuration', () => { const user = createTestUser({ id: 'user-123', diff --git a/packages/api/src/utils/env.ts b/packages/api/src/utils/env.ts index 78d6f9ebdf..adeeb24b34 100644 --- a/packages/api/src/utils/env.ts +++ b/packages/api/src/utils/env.ts @@ -226,9 +226,20 @@ function processSingleValue({ let value = originalValue; + /** + * SECURITY INVARIANT — ordering matters: + * Resolve env vars on the admin-authored template BEFORE any user-controlled + * data is substituted (customUserVars, user fields, OIDC tokens, body placeholders). + * This prevents second-order injection where user values containing ${VAR} + * patterns would otherwise be expanded against process.env. + */ + if (!dbSourced) { + value = extractEnvVariable(value); + } + + /** Runs for both dbSourced and non-dbSourced — it is the only resolution DB-stored servers get */ if (customUserVars) { for (const [varName, varVal] of Object.entries(customUserVars)) { - /** Escaped varName for use in regex to avoid issues with special characters */ const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const placeholderRegex = new RegExp(`\\{\\{${escapedVarName}\\}\\}`, 'g'); value = value.replace(placeholderRegex, varVal); @@ -250,8 +261,6 @@ function processSingleValue({ value = processBodyPlaceholders(value, body); } - value = extractEnvVariable(value); - return value; } diff --git a/packages/data-provider/specs/utils.spec.ts b/packages/data-provider/specs/utils.spec.ts index 01c403f4e8..48df6d46c2 100644 --- a/packages/data-provider/specs/utils.spec.ts +++ b/packages/data-provider/specs/utils.spec.ts @@ -1,4 +1,4 @@ -import { extractEnvVariable } from '../src/utils'; +import { extractEnvVariable, isSensitiveEnvVar } from '../src/utils'; describe('Environment Variable Extraction', () => { const originalEnv = process.env; @@ -7,7 +7,7 @@ describe('Environment Variable Extraction', () => { process.env = { ...originalEnv, TEST_API_KEY: 'test-api-key-value', - ANOTHER_SECRET: 'another-secret-value', + ANOTHER_VALUE: 'another-value', }; }); @@ -55,7 +55,7 @@ describe('Environment Variable Extraction', () => { describe('extractEnvVariable function', () => { it('should extract environment variables from exact matches', () => { expect(extractEnvVariable('${TEST_API_KEY}')).toBe('test-api-key-value'); - expect(extractEnvVariable('${ANOTHER_SECRET}')).toBe('another-secret-value'); + expect(extractEnvVariable('${ANOTHER_VALUE}')).toBe('another-value'); }); it('should extract environment variables from strings with prefixes', () => { @@ -82,7 +82,7 @@ describe('Environment Variable Extraction', () => { describe('extractEnvVariable', () => { it('should extract environment variable values', () => { expect(extractEnvVariable('${TEST_API_KEY}')).toBe('test-api-key-value'); - expect(extractEnvVariable('${ANOTHER_SECRET}')).toBe('another-secret-value'); + expect(extractEnvVariable('${ANOTHER_VALUE}')).toBe('another-value'); }); it('should return the original string if environment variable is not found', () => { @@ -126,4 +126,71 @@ describe('Environment Variable Extraction', () => { ); }); }); + + describe('isSensitiveEnvVar', () => { + it('should flag infrastructure secrets', () => { + expect(isSensitiveEnvVar('JWT_SECRET')).toBe(true); + expect(isSensitiveEnvVar('JWT_REFRESH_SECRET')).toBe(true); + expect(isSensitiveEnvVar('CREDS_KEY')).toBe(true); + expect(isSensitiveEnvVar('CREDS_IV')).toBe(true); + expect(isSensitiveEnvVar('MEILI_MASTER_KEY')).toBe(true); + expect(isSensitiveEnvVar('MONGO_URI')).toBe(true); + expect(isSensitiveEnvVar('REDIS_URI')).toBe(true); + expect(isSensitiveEnvVar('REDIS_PASSWORD')).toBe(true); + }); + + it('should allow non-infrastructure vars through (including operator-configured secrets)', () => { + expect(isSensitiveEnvVar('OPENAI_API_KEY')).toBe(false); + expect(isSensitiveEnvVar('ANTHROPIC_API_KEY')).toBe(false); + expect(isSensitiveEnvVar('GOOGLE_KEY')).toBe(false); + expect(isSensitiveEnvVar('PROXY')).toBe(false); + expect(isSensitiveEnvVar('DEBUG_LOGGING')).toBe(false); + expect(isSensitiveEnvVar('DOMAIN_CLIENT')).toBe(false); + expect(isSensitiveEnvVar('APP_TITLE')).toBe(false); + expect(isSensitiveEnvVar('OPENID_CLIENT_SECRET')).toBe(false); + expect(isSensitiveEnvVar('DISCORD_CLIENT_SECRET')).toBe(false); + expect(isSensitiveEnvVar('MY_CUSTOM_SECRET')).toBe(false); + }); + }); + + describe('extractEnvVariable sensitive var blocklist', () => { + beforeEach(() => { + process.env.JWT_SECRET = 'super-secret-jwt'; + process.env.JWT_REFRESH_SECRET = 'super-secret-refresh'; + process.env.CREDS_KEY = 'encryption-key'; + process.env.CREDS_IV = 'encryption-iv'; + process.env.MEILI_MASTER_KEY = 'meili-key'; + process.env.MONGO_URI = 'mongodb://user:pass@host/db'; + process.env.REDIS_URI = 'redis://:pass@host:6379'; + process.env.REDIS_PASSWORD = 'redis-pass'; + process.env.OPENAI_API_KEY = 'sk-legit-key'; + }); + + it('should refuse to resolve sensitive vars (single-match path)', () => { + expect(extractEnvVariable('${JWT_SECRET}')).toBe('${JWT_SECRET}'); + expect(extractEnvVariable('${JWT_REFRESH_SECRET}')).toBe('${JWT_REFRESH_SECRET}'); + expect(extractEnvVariable('${CREDS_KEY}')).toBe('${CREDS_KEY}'); + expect(extractEnvVariable('${CREDS_IV}')).toBe('${CREDS_IV}'); + expect(extractEnvVariable('${MEILI_MASTER_KEY}')).toBe('${MEILI_MASTER_KEY}'); + expect(extractEnvVariable('${MONGO_URI}')).toBe('${MONGO_URI}'); + expect(extractEnvVariable('${REDIS_URI}')).toBe('${REDIS_URI}'); + expect(extractEnvVariable('${REDIS_PASSWORD}')).toBe('${REDIS_PASSWORD}'); + }); + + it('should refuse to resolve sensitive vars in composite strings (multi-match path)', () => { + expect(extractEnvVariable('key=${JWT_SECRET}&more')).toBe('key=${JWT_SECRET}&more'); + expect(extractEnvVariable('db=${MONGO_URI}/extra')).toBe('db=${MONGO_URI}/extra'); + }); + + it('should still resolve non-sensitive vars normally', () => { + expect(extractEnvVariable('${OPENAI_API_KEY}')).toBe('sk-legit-key'); + expect(extractEnvVariable('Bearer ${OPENAI_API_KEY}')).toBe('Bearer sk-legit-key'); + }); + + it('should resolve non-sensitive vars while blocking sensitive ones in the same string', () => { + expect(extractEnvVariable('key=${OPENAI_API_KEY}&secret=${JWT_SECRET}')).toBe( + 'key=sk-legit-key&secret=${JWT_SECRET}', + ); + }); + }); }); diff --git a/packages/data-provider/src/utils.ts b/packages/data-provider/src/utils.ts index 57abbf0495..1eefcff8c4 100644 --- a/packages/data-provider/src/utils.ts +++ b/packages/data-provider/src/utils.ts @@ -1,5 +1,29 @@ export const envVarRegex = /^\${(.+)}$/; +/** + * Infrastructure env vars that must never be resolved via placeholder expansion. + * These are internal secrets whose exposure would compromise the system — + * they have no legitimate reason to appear in outbound headers, MCP env/args, or OAuth config. + * + * Intentionally excludes API keys (operators reference them in config) and + * OAuth/session secrets (referenced in MCP OAuth config via processMCPEnv). + */ +const SENSITIVE_ENV_VARS = new Set([ + 'JWT_SECRET', + 'JWT_REFRESH_SECRET', + 'CREDS_KEY', + 'CREDS_IV', + 'MEILI_MASTER_KEY', + 'MONGO_URI', + 'REDIS_URI', + 'REDIS_PASSWORD', +]); + +/** Returns true when `varName` refers to an infrastructure secret that must not leak. */ +export function isSensitiveEnvVar(varName: string): boolean { + return SENSITIVE_ENV_VARS.has(varName); +} + /** Extracts the environment variable name from a template literal string */ export function extractVariableName(value: string): string | null { if (!value) { @@ -16,21 +40,20 @@ export function extractEnvVariable(value: string) { return value; } - // Trim the input const trimmed = value.trim(); - // Special case: if it's just a single environment variable const singleMatch = trimmed.match(envVarRegex); if (singleMatch) { const varName = singleMatch[1]; + if (isSensitiveEnvVar(varName)) { + return trimmed; + } return process.env[varName] || trimmed; } - // For multiple variables, process them using a regex loop const regex = /\${([^}]+)}/g; let result = trimmed; - // First collect all matches and their positions const matches = []; let match; while ((match = regex.exec(trimmed)) !== null) { @@ -41,12 +64,12 @@ export function extractEnvVariable(value: string) { }); } - // Process matches in reverse order to avoid position shifts for (let i = matches.length - 1; i >= 0; i--) { const { fullMatch, varName, index } = matches[i]; + if (isSensitiveEnvVar(varName)) { + continue; + } const envValue = process.env[varName] || fullMatch; - - // Replace at exact position result = result.substring(0, index) + envValue + result.substring(index + fullMatch.length); } From 381ed8539bd14e8099e8d0d9b6c093c751a57b25 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 16 Mar 2026 09:19:48 -0400 Subject: [PATCH 055/111] =?UTF-8?q?=F0=9F=AA=AA=20fix:=20Enforce=20Convers?= =?UTF-8?q?ation=20Ownership=20Checks=20in=20Remote=20Agent=20Controllers?= =?UTF-8?q?=20(#12263)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 fix: Validate conversation ownership in remote agent API endpoints Add user-scoped ownership checks for client-supplied conversation IDs in OpenAI-compatible and Open Responses controllers to prevent cross-tenant file/message loading via IDOR. * 🔒 fix: Harden ownership checks against type confusion and unhandled errors - Add typeof string validation before getConvo to block NoSQL operator injection (e.g. { "$gt": "" }) bypassing the ownership check - Move ownership checks inside try/catch so DB errors produce structured JSON error responses instead of unhandled promise rejections - Add string type validation for conversation_id and previous_response_id in the upstream TS request validators (defense-in-depth) * 🧪 test: Add coverage for conversation ownership validation in remote agent APIs - Fix broken getConvo mock in openai.spec.js (was missing entirely) - Add tests for: owned conversation, unowned (404), non-string type (400), absent conversation_id (skipped), and DB error (500) — both controllers --- .../agents/__tests__/openai.spec.js | 72 ++++++++++++++ .../agents/__tests__/responses.unit.spec.js | 96 +++++++++++++++++++ api/server/controllers/agents/openai.js | 21 +++- api/server/controllers/agents/responses.js | 21 +++- packages/api/src/agents/openai/service.ts | 8 ++ packages/api/src/agents/responses/service.ts | 7 ++ 6 files changed, 218 insertions(+), 7 deletions(-) diff --git a/api/server/controllers/agents/__tests__/openai.spec.js b/api/server/controllers/agents/__tests__/openai.spec.js index 835343e798..50c61b7288 100644 --- a/api/server/controllers/agents/__tests__/openai.spec.js +++ b/api/server/controllers/agents/__tests__/openai.spec.js @@ -99,6 +99,7 @@ jest.mock('~/server/services/PermissionService', () => ({ jest.mock('~/models/Conversation', () => ({ getConvoFiles: jest.fn().mockResolvedValue([]), + getConvo: jest.fn().mockResolvedValue(null), })); jest.mock('~/models/Agent', () => ({ @@ -160,6 +161,77 @@ describe('OpenAIChatCompletionController', () => { }; }); + describe('conversation ownership validation', () => { + it('should skip ownership check when conversation_id is not provided', async () => { + const { getConvo } = require('~/models/Conversation'); + await OpenAIChatCompletionController(req, res); + expect(getConvo).not.toHaveBeenCalled(); + }); + + it('should return 400 when conversation_id is not a string', async () => { + const { validateRequest } = require('@librechat/api'); + validateRequest.mockReturnValueOnce({ + request: { model: 'agent-123', messages: [], stream: false, conversation_id: { $gt: '' } }, + }); + + await OpenAIChatCompletionController(req, res); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it('should return 404 when conversation is not owned by user', async () => { + const { validateRequest } = require('@librechat/api'); + const { getConvo } = require('~/models/Conversation'); + validateRequest.mockReturnValueOnce({ + request: { + model: 'agent-123', + messages: [], + stream: false, + conversation_id: 'convo-abc', + }, + }); + getConvo.mockResolvedValueOnce(null); + + await OpenAIChatCompletionController(req, res); + expect(getConvo).toHaveBeenCalledWith('user-123', 'convo-abc'); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it('should proceed when conversation is owned by user', async () => { + const { validateRequest } = require('@librechat/api'); + const { getConvo } = require('~/models/Conversation'); + validateRequest.mockReturnValueOnce({ + request: { + model: 'agent-123', + messages: [], + stream: false, + conversation_id: 'convo-abc', + }, + }); + getConvo.mockResolvedValueOnce({ conversationId: 'convo-abc', user: 'user-123' }); + + await OpenAIChatCompletionController(req, res); + expect(getConvo).toHaveBeenCalledWith('user-123', 'convo-abc'); + expect(res.status).not.toHaveBeenCalledWith(404); + }); + + it('should return 500 when getConvo throws a DB error', async () => { + const { validateRequest } = require('@librechat/api'); + const { getConvo } = require('~/models/Conversation'); + validateRequest.mockReturnValueOnce({ + request: { + model: 'agent-123', + messages: [], + stream: false, + conversation_id: 'convo-abc', + }, + }); + getConvo.mockRejectedValueOnce(new Error('DB connection failed')); + + await OpenAIChatCompletionController(req, res); + expect(res.status).toHaveBeenCalledWith(500); + }); + }); + describe('token usage recording', () => { it('should call recordCollectedUsage after successful non-streaming completion', async () => { await OpenAIChatCompletionController(req, res); diff --git a/api/server/controllers/agents/__tests__/responses.unit.spec.js b/api/server/controllers/agents/__tests__/responses.unit.spec.js index 45ec31fc68..e34f0ccf73 100644 --- a/api/server/controllers/agents/__tests__/responses.unit.spec.js +++ b/api/server/controllers/agents/__tests__/responses.unit.spec.js @@ -189,6 +189,102 @@ describe('createResponse controller', () => { }; }); + describe('conversation ownership validation', () => { + it('should skip ownership check when previous_response_id is not provided', async () => { + const { getConvo } = require('~/models/Conversation'); + await createResponse(req, res); + expect(getConvo).not.toHaveBeenCalled(); + }); + + it('should return 400 when previous_response_id is not a string', async () => { + const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); + validateResponseRequest.mockReturnValueOnce({ + request: { + model: 'agent-123', + input: 'Hello', + stream: false, + previous_response_id: { $gt: '' }, + }, + }); + + await createResponse(req, res); + expect(sendResponsesErrorResponse).toHaveBeenCalledWith( + res, + 400, + 'previous_response_id must be a string', + 'invalid_request', + ); + }); + + it('should return 404 when conversation is not owned by user', async () => { + const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); + const { getConvo } = require('~/models/Conversation'); + validateResponseRequest.mockReturnValueOnce({ + request: { + model: 'agent-123', + input: 'Hello', + stream: false, + previous_response_id: 'resp_abc', + }, + }); + getConvo.mockResolvedValueOnce(null); + + await createResponse(req, res); + expect(getConvo).toHaveBeenCalledWith('user-123', 'resp_abc'); + expect(sendResponsesErrorResponse).toHaveBeenCalledWith( + res, + 404, + 'Conversation not found', + 'not_found', + ); + }); + + it('should proceed when conversation is owned by user', async () => { + const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); + const { getConvo } = require('~/models/Conversation'); + validateResponseRequest.mockReturnValueOnce({ + request: { + model: 'agent-123', + input: 'Hello', + stream: false, + previous_response_id: 'resp_abc', + }, + }); + getConvo.mockResolvedValueOnce({ conversationId: 'resp_abc', user: 'user-123' }); + + await createResponse(req, res); + expect(getConvo).toHaveBeenCalledWith('user-123', 'resp_abc'); + expect(sendResponsesErrorResponse).not.toHaveBeenCalledWith( + res, + 404, + expect.any(String), + expect.any(String), + ); + }); + + it('should return 500 when getConvo throws a DB error', async () => { + const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); + const { getConvo } = require('~/models/Conversation'); + validateResponseRequest.mockReturnValueOnce({ + request: { + model: 'agent-123', + input: 'Hello', + stream: false, + previous_response_id: 'resp_abc', + }, + }); + getConvo.mockRejectedValueOnce(new Error('DB connection failed')); + + await createResponse(req, res); + expect(sendResponsesErrorResponse).toHaveBeenCalledWith( + res, + 500, + expect.any(String), + expect.any(String), + ); + }); + }); + describe('token usage recording - non-streaming', () => { it('should call recordCollectedUsage after successful non-streaming completion', async () => { await createResponse(req, res); diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index bab81f1535..189cb29d8d 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -26,7 +26,7 @@ const { createToolEndCallback } = require('~/server/controllers/agents/callbacks const { findAccessibleResources } = require('~/server/services/PermissionService'); const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); -const { getConvoFiles } = require('~/models/Conversation'); +const { getConvoFiles, getConvo } = require('~/models/Conversation'); const { getAgent, getAgents } = require('~/models/Agent'); const db = require('~/models'); @@ -151,8 +151,6 @@ const OpenAIChatCompletionController = async (req, res) => { } const responseId = `chatcmpl-${nanoid()}`; - const conversationId = request.conversation_id ?? nanoid(); - const parentMessageId = request.parent_message_id ?? null; const created = Math.floor(Date.now() / 1000); /** @type {import('@librechat/api').OpenAIResponseContext} — key must be `requestId` to match the type used by createChunk/buildNonStreamingResponse */ @@ -178,6 +176,23 @@ const OpenAIChatCompletionController = async (req, res) => { }); try { + if (request.conversation_id != null) { + if (typeof request.conversation_id !== 'string') { + return sendErrorResponse( + res, + 400, + 'conversation_id must be a string', + 'invalid_request_error', + ); + } + if (!(await getConvo(req.user?.id, request.conversation_id))) { + return sendErrorResponse(res, 404, 'Conversation not found', 'invalid_request_error'); + } + } + + const conversationId = request.conversation_id ?? nanoid(); + const parentMessageId = request.parent_message_id ?? null; + // Build allowed providers set const allowedProviders = new Set( appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders, diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index bbf02580dd..30ccacdba8 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -292,10 +292,6 @@ const createResponse = async (req, res) => { // Generate IDs const responseId = generateResponseId(); - const conversationId = request.previous_response_id ?? uuidv4(); - const parentMessageId = null; - - // Create response context const context = createResponseContext(request, responseId); logger.debug( @@ -314,6 +310,23 @@ const createResponse = async (req, res) => { }); try { + if (request.previous_response_id != null) { + if (typeof request.previous_response_id !== 'string') { + return sendResponsesErrorResponse( + res, + 400, + 'previous_response_id must be a string', + 'invalid_request', + ); + } + if (!(await getConvo(req.user?.id, request.previous_response_id))) { + return sendResponsesErrorResponse(res, 404, 'Conversation not found', 'not_found'); + } + } + + const conversationId = request.previous_response_id ?? uuidv4(); + const parentMessageId = null; + // Build allowed providers set const allowedProviders = new Set( appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders, diff --git a/packages/api/src/agents/openai/service.ts b/packages/api/src/agents/openai/service.ts index 807ce8db71..90190ce7ce 100644 --- a/packages/api/src/agents/openai/service.ts +++ b/packages/api/src/agents/openai/service.ts @@ -289,6 +289,14 @@ export function validateRequest(body: unknown): ChatCompletionValidationResult { } } + if (request.conversation_id !== undefined && typeof request.conversation_id !== 'string') { + return { valid: false, error: 'conversation_id must be a string' }; + } + + if (request.parent_message_id !== undefined && typeof request.parent_message_id !== 'string') { + return { valid: false, error: 'parent_message_id must be a string' }; + } + return { valid: true, request: request as unknown as ChatCompletionRequest }; } diff --git a/packages/api/src/agents/responses/service.ts b/packages/api/src/agents/responses/service.ts index 2e49b1b979..575606123c 100644 --- a/packages/api/src/agents/responses/service.ts +++ b/packages/api/src/agents/responses/service.ts @@ -84,6 +84,13 @@ export function validateResponseRequest(body: unknown): RequestValidationResult } } + if ( + request.previous_response_id !== undefined && + typeof request.previous_response_id !== 'string' + ) { + return { valid: false, error: 'previous_response_id must be a string' }; + } + return { valid: true, request: request as unknown as ResponseRequest }; } From d17ac8f06dd4956f29e657a068f4cc5a241adf08 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 16 Mar 2026 09:23:46 -0400 Subject: [PATCH 056/111] =?UTF-8?q?=F0=9F=94=8F=20fix:=20Remove=20Federate?= =?UTF-8?q?d=20Tokens=20from=20OpenID=20Refresh=20Response=20(#12264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 fix: Remove OpenID federated tokens from refresh endpoint response The refresh controller was attaching federatedTokens (including the refresh_token) to the user object returned in the JSON response, exposing HttpOnly-protected tokens to client-side JavaScript. The tokens are already stored server-side by setOpenIDAuthTokens and re-attached by the JWT strategy on authenticated requests. * 🔒 fix: Strip sensitive fields from OpenID refresh response user object The OpenID refresh path returned the raw findOpenIDUser result without field projection, unlike the non-OpenID path which excludes password, __v, totpSecret, and backupCodes via getUserById projection. Destructure out sensitive fields before serializing. Also strengthens the regression test: uses not.toHaveProperty for true property-absence checks (expect.anything() misses null/undefined), adds positive shape assertion, and DRYs up duplicated mock user setup. --- api/server/controllers/AuthController.js | 10 +---- api/server/controllers/AuthController.spec.js | 44 +++++++++++++------ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 13d024cd03..eb44feffa4 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -119,14 +119,8 @@ const refreshController = async (req, res) => { const token = setOpenIDAuthTokens(tokenset, req, res, user._id.toString(), refreshToken); - user.federatedTokens = { - access_token: tokenset.access_token, - id_token: tokenset.id_token, - refresh_token: refreshToken, - expires_at: claims.exp, - }; - - return res.status(200).send({ token, user }); + const { password: _pw, __v: _v, totpSecret: _ts, backupCodes: _bc, ...safeUser } = user; + return res.status(200).send({ token, user: safeUser }); } catch (error) { logger.error('[refreshController] OpenID token refresh error', error); return res.status(403).send('Invalid OpenID refresh token'); diff --git a/api/server/controllers/AuthController.spec.js b/api/server/controllers/AuthController.spec.js index fef670baa8..964947def9 100644 --- a/api/server/controllers/AuthController.spec.js +++ b/api/server/controllers/AuthController.spec.js @@ -163,6 +163,16 @@ describe('refreshController – OpenID path', () => { exp: 9999999999, }; + const defaultUser = { + _id: 'user-db-id', + email: baseClaims.email, + openidId: baseClaims.sub, + password: '$2b$10$hashedpassword', + __v: 0, + totpSecret: 'encrypted-totp-secret', + backupCodes: ['hashed-code-1', 'hashed-code-2'], + }; + let req, res; beforeEach(() => { @@ -174,6 +184,7 @@ describe('refreshController – OpenID path', () => { mockTokenset.claims.mockReturnValue(baseClaims); getOpenIdEmail.mockReturnValue(baseClaims.email); setOpenIDAuthTokens.mockReturnValue('new-app-token'); + findOpenIDUser.mockResolvedValue({ user: { ...defaultUser }, error: null, migration: false }); updateUser.mockResolvedValue({}); req = { @@ -189,13 +200,6 @@ describe('refreshController – OpenID path', () => { }); it('should call getOpenIdEmail with token claims and use result for findOpenIDUser', async () => { - const user = { - _id: 'user-db-id', - email: baseClaims.email, - openidId: baseClaims.sub, - }; - findOpenIDUser.mockResolvedValue({ user, error: null, migration: false }); - await refreshController(req, res); expect(getOpenIdEmail).toHaveBeenCalledWith(baseClaims); @@ -229,13 +233,6 @@ describe('refreshController – OpenID path', () => { it('should fall back to claims.email when configured claim is absent from token claims', async () => { getOpenIdEmail.mockReturnValue(baseClaims.email); - const user = { - _id: 'user-db-id', - email: baseClaims.email, - openidId: baseClaims.sub, - }; - findOpenIDUser.mockResolvedValue({ user, error: null, migration: false }); - await refreshController(req, res); expect(findOpenIDUser).toHaveBeenCalledWith( @@ -243,6 +240,25 @@ describe('refreshController – OpenID path', () => { ); }); + it('should not expose sensitive fields or federatedTokens in refresh response', async () => { + await refreshController(req, res); + + const sentPayload = res.send.mock.calls[0][0]; + expect(sentPayload).toEqual({ + token: 'new-app-token', + user: expect.objectContaining({ + _id: 'user-db-id', + email: baseClaims.email, + openidId: baseClaims.sub, + }), + }); + expect(sentPayload.user).not.toHaveProperty('federatedTokens'); + expect(sentPayload.user).not.toHaveProperty('password'); + expect(sentPayload.user).not.toHaveProperty('totpSecret'); + expect(sentPayload.user).not.toHaveProperty('backupCodes'); + expect(sentPayload.user).not.toHaveProperty('__v'); + }); + it('should update openidId when migration is triggered on refresh', async () => { const user = { _id: 'user-db-id', email: baseClaims.email, openidId: null }; findOpenIDUser.mockResolvedValue({ user, error: null, migration: true }); From c68066a63690cb3c601fd379edfe1d7df5f7b5d4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 16 Mar 2026 09:31:01 -0400 Subject: [PATCH 057/111] =?UTF-8?q?=F0=9F=AA=9D=20fix:=20MCP=20Refresh=20t?= =?UTF-8?q?oken=20on=20OAuth=20Discovery=20Failure=20(#12266)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 fix: Prevent token leaks to MCP server on OAuth discovery failure When OAuth metadata discovery fails, refresh logic was falling back to POSTing refresh tokens to /token on the MCP resource server URL instead of the authorization server. A malicious MCP server could exploit this by blocking .well-known discovery to harvest refresh tokens. Changes: - Replace unsafe /token fallback with hard error in both refresh paths - Thread stored token_endpoint (SSRF-validated during initial flow) through the refresh chain so legacy servers without .well-known still work after the first successful auth - Fix revokeOAuthToken to always SSRF-validate the revocation URL, including the /revoke fallback path - Redact refresh token and credentials from debug-level log output - Split branch 2 compound condition for consistent error messages * ✅ test: Add stored endpoint fallback tests and improve refresh coverage - Add storedTokenEndpoint fallback tests for both refresh branches - Add missing test for branch 2 metadata-without-token_endpoint case - Rename misleading test name to match actual mock behavior - Split auto-discovered throw test into undefined vs missing-endpoint - Remove redundant afterEach mockFetch.mockClear() calls (already covered by jest.clearAllMocks() in beforeEach) --- packages/api/src/mcp/MCPConnectionFactory.ts | 4 + .../api/src/mcp/__tests__/handler.test.ts | 247 ++++++++++++------ packages/api/src/mcp/oauth/handler.ts | 60 +++-- packages/api/src/mcp/oauth/tokens.ts | 20 +- 4 files changed, 215 insertions(+), 116 deletions(-) diff --git a/packages/api/src/mcp/MCPConnectionFactory.ts b/packages/api/src/mcp/MCPConnectionFactory.ts index b5b3d61bf0..2c16da0760 100644 --- a/packages/api/src/mcp/MCPConnectionFactory.ts +++ b/packages/api/src/mcp/MCPConnectionFactory.ts @@ -287,6 +287,8 @@ export class MCPConnectionFactory { serverName: string; identifier: string; clientInfo?: OAuthClientInformation; + storedTokenEndpoint?: string; + storedAuthMethods?: string[]; }, ) => Promise { return async (refreshToken, metadata) => { @@ -296,6 +298,8 @@ export class MCPConnectionFactory { serverUrl: (this.serverConfig as t.SSEOptions | t.StreamableHTTPOptions).url, serverName: metadata.serverName, clientInfo: metadata.clientInfo, + storedTokenEndpoint: metadata.storedTokenEndpoint, + storedAuthMethods: metadata.storedAuthMethods, }, this.serverConfig.oauth_headers ?? {}, this.serverConfig.oauth, diff --git a/packages/api/src/mcp/__tests__/handler.test.ts b/packages/api/src/mcp/__tests__/handler.test.ts index 3b68d88e9c..31665ce8f7 100644 --- a/packages/api/src/mcp/__tests__/handler.test.ts +++ b/packages/api/src/mcp/__tests__/handler.test.ts @@ -260,10 +260,6 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { global.fetch = mockFetch; }); - afterEach(() => { - mockFetch.mockClear(); - }); - afterAll(() => { global.fetch = originalFetch; }); @@ -679,6 +675,109 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { 'Token refresh failed: 400 Bad Request - {"error":"invalid_request","error_description":"refresh_token.client_id: Field required"}', ); }); + + describe('stored token endpoint fallback', () => { + it('uses stored token endpoint when discovery fails (stored clientInfo)', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://mcp.example.com', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + storedTokenEndpoint: 'https://auth.example.com/token', + storedAuthMethods: ['client_secret_basic'], + }; + + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://auth.example.com/token', + expect.objectContaining({ method: 'POST' }), + ); + expect(result.access_token).toBe('new-access-token'); + }); + + it('uses stored token endpoint when discovery fails (auto-discovered)', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://mcp.example.com', + storedTokenEndpoint: 'https://auth.example.com/token', + }; + + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + const [fetchUrl] = mockFetch.mock.calls[0]; + expect(fetchUrl).toBeInstanceOf(URL); + expect(fetchUrl.toString()).toBe('https://auth.example.com/token'); + expect(result.access_token).toBe('new-access-token'); + }); + + it('still throws when discovery fails and no stored endpoint (stored clientInfo)', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://mcp.example.com', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + }; + + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No OAuth metadata discovered for token refresh'); + + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('still throws when discovery fails and no stored endpoint (auto-discovered)', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://mcp.example.com', + }; + + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No OAuth metadata discovered for token refresh'); + + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); }); describe('revokeOAuthToken', () => { @@ -1187,10 +1286,6 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { global.fetch = mockFetch; }); - afterEach(() => { - mockFetch.mockClear(); - }); - afterAll(() => { global.fetch = originalFetch; }); @@ -1363,7 +1458,7 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { ); }); - it('should use fallback /token endpoint for refresh when metadata discovery fails', async () => { + it('should throw when metadata discovery fails during refresh (stored clientInfo)', async () => { const metadata = { serverName: 'test-server', serverUrl: 'https://mcp.example.com', @@ -1373,38 +1468,16 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { }, }; - // Mock metadata discovery to return undefined (no .well-known) mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); - // Mock successful token refresh - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ - access_token: 'new-access-token', - refresh_token: 'new-refresh-token', - expires_in: 3600, - }), - } as Response); + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No OAuth metadata discovered for token refresh'); - const result = await MCPOAuthHandler.refreshOAuthTokens( - 'test-refresh-token', - metadata, - {}, - {}, - ); - - // Verify fetch was called with fallback /token endpoint - expect(mockFetch).toHaveBeenCalledWith( - 'https://mcp.example.com/token', - expect.objectContaining({ - method: 'POST', - }), - ); - - expect(result.access_token).toBe('new-access-token'); + expect(mockFetch).not.toHaveBeenCalled(); }); - it('should use fallback auth methods when metadata discovery fails during refresh', async () => { + it('should throw when metadata lacks token endpoint during refresh', async () => { const metadata = { serverName: 'test-server', serverUrl: 'https://mcp.example.com', @@ -1414,30 +1487,51 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { }, }; - // Mock metadata discovery to return undefined + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce({ + issuer: 'https://auth.example.com/', + authorization_endpoint: 'https://auth.example.com/authorize', + response_types_supported: ['code'], + } as AuthorizationServerMetadata); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No token endpoint found in OAuth metadata'); + + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should throw for auto-discovered refresh when metadata discovery returns undefined', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://mcp.example.com', + }; + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); - // Mock successful token refresh - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ - access_token: 'new-access-token', - expires_in: 3600, - }), - } as Response); + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No OAuth metadata discovered for token refresh'); - await MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}); + expect(mockFetch).not.toHaveBeenCalled(); + }); - // Verify it uses client_secret_basic (first in fallback auth methods) - const expectedAuth = `Basic ${Buffer.from('test-client-id:test-client-secret').toString('base64')}`; - expect(mockFetch).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: expectedAuth, - }), - }), - ); + it('should throw for auto-discovered refresh when metadata has no token_endpoint', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://mcp.example.com', + }; + + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce({ + issuer: 'https://auth.example.com/', + authorization_endpoint: 'https://auth.example.com/authorize', + response_types_supported: ['code'], + } as AuthorizationServerMetadata); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No token endpoint found in OAuth metadata'); + + expect(mockFetch).not.toHaveBeenCalled(); }); describe('path-based URL origin fallback', () => { @@ -1574,7 +1668,7 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { expect(result.access_token).toBe('new-access-token'); }); - it('falls back to /token when both path and origin discovery fail', async () => { + it('throws when both path and origin discovery return undefined', async () => { const metadata = { serverName: 'sentry', serverUrl: 'https://mcp.sentry.dev/mcp', @@ -1585,36 +1679,19 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { }, }; - // Both path AND origin discovery return undefined mockDiscoverAuthorizationServerMetadata .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(undefined); - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ - access_token: 'new-access-token', - refresh_token: 'new-refresh-token', - expires_in: 3600, - }), - } as Response); - - const result = await MCPOAuthHandler.refreshOAuthTokens( - 'test-refresh-token', - metadata, - {}, - {}, - ); + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No OAuth metadata discovered for token refresh'); expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); - - // Falls back to /token relative to server URL origin - const [fetchUrl] = mockFetch.mock.calls[0]; - expect(String(fetchUrl)).toBe('https://mcp.sentry.dev/token'); - expect(result.access_token).toBe('new-access-token'); + expect(mockFetch).not.toHaveBeenCalled(); }); - it('does not retry with origin when server URL has no path (root URL)', async () => { + it('throws when root URL discovery returns undefined (no path retry)', async () => { const metadata = { serverName: 'test-server', serverUrl: 'https://auth.example.com/', @@ -1624,18 +1701,14 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { }, }; - // Root URL discovery fails — no retry mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ access_token: 'new-token', expires_in: 3600 }), - } as Response); + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('No OAuth metadata discovered for token refresh'); - await MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}); - - // Only one discovery attempt for a root URL expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(1); + expect(mockFetch).not.toHaveBeenCalled(); }); it('retries with origin when path-based discovery throws', async () => { diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 0a9154ff35..873af5c66d 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -821,7 +821,13 @@ export class MCPOAuthHandler { */ static async refreshOAuthTokens( refreshToken: string, - metadata: { serverName: string; serverUrl?: string; clientInfo?: OAuthClientInformation }, + metadata: { + serverName: string; + serverUrl?: string; + clientInfo?: OAuthClientInformation; + storedTokenEndpoint?: string; + storedAuthMethods?: string[]; + }, oauthHeaders: Record, config?: MCPOptions['oauth'], allowedDomains?: string[] | null, @@ -862,15 +868,18 @@ export class MCPOAuthHandler { const oauthMetadata = await this.discoverWithOriginFallback(serverUrl, fetchFn); if (!oauthMetadata) { - /** - * No metadata discovered - use fallback /token endpoint. - * This mirrors the MCP SDK's behavior for legacy servers without .well-known endpoints. - */ - logger.warn( - `[MCPOAuth] No OAuth metadata discovered for token refresh, using fallback /token endpoint`, - ); - tokenUrl = new URL('/token', metadata.serverUrl).toString(); - authMethods = ['client_secret_basic', 'client_secret_post', 'none']; + if (metadata.storedTokenEndpoint) { + tokenUrl = metadata.storedTokenEndpoint; + authMethods = metadata.storedAuthMethods; + } else { + /** + * Do NOT fall back to `new URL('/token', metadata.serverUrl)`. + * metadata.serverUrl is the MCP resource server, which may differ from the + * authorization server. Sending refresh tokens there leaks them to the + * resource server operator when .well-known discovery is absent. + */ + throw new Error('No OAuth metadata discovered for token refresh'); + } } else if (!oauthMetadata.token_endpoint) { throw new Error('No token endpoint found in OAuth metadata'); } else { @@ -930,8 +939,8 @@ export class MCPOAuthHandler { } logger.debug(`[MCPOAuth] Refresh request to: ${sanitizeUrlForLogging(tokenUrl)}`, { - body: body.toString(), - headers, + grant_type: 'refresh_token', + has_auth_header: !!headers['Authorization'], }); const response = await fetch(tokenUrl, { @@ -1040,15 +1049,16 @@ export class MCPOAuthHandler { const oauthMetadata = await this.discoverWithOriginFallback(serverUrl, fetchFn); let tokenUrl: URL; - if (!oauthMetadata?.token_endpoint) { - /** - * No metadata or token_endpoint discovered - use fallback /token endpoint. - * This mirrors the MCP SDK's behavior for legacy servers without .well-known endpoints. - */ - logger.warn( - `[MCPOAuth] No OAuth metadata or token endpoint found, using fallback /token endpoint`, - ); - tokenUrl = new URL('/token', metadata.serverUrl); + if (!oauthMetadata) { + if (metadata.storedTokenEndpoint) { + tokenUrl = new URL(metadata.storedTokenEndpoint); + } else { + // Same rationale as the stored-clientInfo branch above: never fall back + // to metadata.serverUrl which is the MCP resource server, not the auth server. + throw new Error('No OAuth metadata discovered for token refresh'); + } + } else if (!oauthMetadata.token_endpoint) { + throw new Error('No token endpoint found in OAuth metadata'); } else { tokenUrl = new URL(oauthMetadata.token_endpoint); } @@ -1103,17 +1113,11 @@ export class MCPOAuthHandler { oauthHeaders: Record = {}, allowedDomains?: string[] | null, ): Promise { - if (metadata.revocationEndpoint != null) { - await this.validateOAuthUrl( - metadata.revocationEndpoint, - 'revocation_endpoint', - allowedDomains, - ); - } const revokeUrl: URL = metadata.revocationEndpoint != null ? new URL(metadata.revocationEndpoint) : new URL('/revoke', metadata.serverUrl); + await this.validateOAuthUrl(revokeUrl.href, 'revocation_endpoint', allowedDomains); const authMethods = metadata.revocationEndpointAuthMethodsSupported ?? ['client_secret_basic']; const authMethod = resolveTokenEndpointAuthMethod({ tokenAuthMethods: authMethods }); diff --git a/packages/api/src/mcp/oauth/tokens.ts b/packages/api/src/mcp/oauth/tokens.ts index 6094a05386..1e31a64511 100644 --- a/packages/api/src/mcp/oauth/tokens.ts +++ b/packages/api/src/mcp/oauth/tokens.ts @@ -41,6 +41,8 @@ interface GetTokensParams { serverName: string; identifier: string; clientInfo?: OAuthClientInformation; + storedTokenEndpoint?: string; + storedAuthMethods?: string[]; }, ) => Promise; createToken?: TokenMethods['createToken']; @@ -306,9 +308,10 @@ export class MCPTokenStorage { logger.info(`${logPrefix} Attempting to refresh token`); const decryptedRefreshToken = await decryptV2(refreshTokenData.token); - /** Client information if available */ let clientInfo; let clientInfoData; + let storedTokenEndpoint: string | undefined; + let storedAuthMethods: string[] | undefined; try { clientInfoData = await findToken({ userId, @@ -322,6 +325,19 @@ export class MCPTokenStorage { client_id: clientInfo.client_id, has_client_secret: !!clientInfo.client_secret, }); + + if (clientInfoData.metadata) { + const raw = + clientInfoData.metadata instanceof Map + ? Object.fromEntries(clientInfoData.metadata) + : (clientInfoData.metadata as Record); + if (typeof raw.token_endpoint === 'string') { + storedTokenEndpoint = raw.token_endpoint; + } + if (Array.isArray(raw.token_endpoint_auth_methods_supported)) { + storedAuthMethods = raw.token_endpoint_auth_methods_supported as string[]; + } + } } } catch { logger.debug(`${logPrefix} No client info found`); @@ -332,6 +348,8 @@ export class MCPTokenStorage { serverName, identifier, clientInfo, + storedTokenEndpoint, + storedAuthMethods, }; const newTokens = await refreshTokens(decryptedRefreshToken, metadata); From 9a64791e3ec8670bbc67d14d6eb5173db64557ef Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 17 Mar 2026 01:38:51 -0400 Subject: [PATCH 058/111] =?UTF-8?q?=F0=9F=AA=A2=20fix:=20Action=20Domain?= =?UTF-8?q?=20Encoding=20Collision=20for=20HTTPS=20URLs=20(#12271)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: strip protocol from domain before encoding in `domainParser` All https:// (and http://) domains produced the same 10-char base64 prefix due to ENCODED_DOMAIN_LENGTH truncation, causing tool name collisions for agents with multiple actions. Strip the protocol before encoding so the base64 key is derived from the hostname. Add `legacyDomainEncode` to preserve the old encoding logic for backward-compatible matching of existing stored actions. * fix: backward-compatible tool matching in ToolService Update `getActionToolDefinitions` to match stored tools against both new and legacy domain encodings. Update `loadActionToolsForExecution` to resolve model-called tool names via a `normalizedToDomain` map that includes both encoding variants, with legacy fallback for request builder lookup. * fix: action route save/delete domain encoding issues Save routes now remove old tools matching either new or legacy domain encoding, preventing stale entries when an action's encoding changes on update. Delete routes no longer re-encode the already-encoded domain extracted from the stored actions array, which was producing incorrect keys and leaving orphaned tools. * test: comprehensive coverage for action domain encoding Rewrite ActionService tests to cover real matching patterns used by ToolService and action routes. Tests verify encode/decode round-trips, protocol stripping, backward-compatible tool name matching at both definition and execution phases, save-route cleanup of old/new encodings, delete-route domain extraction, and the collision fix for multi-action agents. * fix: add legacy domain compat to all execution paths, make legacyDomainEncode sync CRITICAL: processRequiredActions (assistants path) was not updated with legacy domain matching — existing assistants with https:// domain actions would silently fail post-deployment because domainMap only had new encoding. MAJOR: loadAgentTools definitionsOnly=false path had the same issue. Both now use a normalizedToDomain map with legacy+new entries and extract function names via the matched key (not the canonical domain). Also: make legacyDomainEncode synchronous (no async operations), store legacyNormalized in processedActionSets to eliminate recomputation in the per-tool fallback, and hoist domainSeparatorRegex to module level. * refactor: clarify domain variable naming and tool-filter helpers in action routes Rename shadowed 'domain' to 'encodedDomain' to separate raw URL from encoded key in both agent and assistant save routes. Rename shouldRemoveTool to shouldRemoveAgentTool / shouldRemoveAssistantTool to make the distinct data-shape guards explicit. Remove await on now-synchronous legacyDomainEncode. * test: expand coverage for all review findings - Add validateAndUpdateTool tests (protocol-stripping match logic) - Restore unicode domain encode/decode/round-trip tests - Add processRequiredActions matching pattern tests (assistants path) - Add legacy guard skip test for short bare hostnames - Add pre-normalized Set test for definition-phase optimization - Fix corrupt-cache test to assert typeof instead of toBeDefined - Verify legacyDomainEncode is synchronous (not a Promise) - Remove all await on legacyDomainEncode (now sync) 58 tests, up from 44. * fix: address follow-up review findings A-E A: Fix stale JSDoc @returns {Promise} on now-synchronous legacyDomainEncode — changed to @returns {string}. B: Rename normalizedToDomain to domainLookupMap in processRequiredActions and loadAgentTools where keys are raw encoded domains (not normalized), avoiding confusion with loadActionToolsForExecution where keys ARE normalized. C: Pre-normalize actionToolNames into a Set in getActionToolDefinitions, replacing O(signatures × tools) per-check .some() + .replace() with O(1) Set.has() lookups. D: Remove stripProtocol from ActionService exports — it is a one-line internal helper. Spec tests for it removed; behavior is fully covered by domainParser protocol-stripping tests. E: Fix pre-existing bug where processRequiredActions re-loaded action sets on every missing-tool iteration. The guard !actionSets.length always re-triggered because actionSets was reassigned to a plain object (whose .length is undefined). Replaced with a null-check on a dedicated actionSetsData variable. * fix: strip path and query from domain URLs in stripProtocol URLs like 'https://api.example.com/v1/endpoint?foo=bar' previously retained the path after protocol stripping, contaminating the encoded domain key. Now strips everything after the first '/' following the host, using string indexing instead of URL parsing to avoid punycode normalization of unicode hostnames. Closes Copilot review comments 1, 2, and 5. --- api/server/routes/agents/actions.js | 40 +- api/server/routes/assistants/actions.js | 49 +- api/server/services/ActionService.js | 56 +- api/server/services/ActionService.spec.js | 662 +++++++++++++++++----- api/server/services/ToolService.js | 113 ++-- 5 files changed, 693 insertions(+), 227 deletions(-) diff --git a/api/server/routes/agents/actions.js b/api/server/routes/agents/actions.js index 4643f096aa..f3970bff22 100644 --- a/api/server/routes/agents/actions.js +++ b/api/server/routes/agents/actions.js @@ -12,7 +12,11 @@ const { validateActionDomain, validateAndParseOpenAPISpec, } = require('librechat-data-provider'); -const { encryptMetadata, domainParser } = require('~/server/services/ActionService'); +const { + legacyDomainEncode, + encryptMetadata, + domainParser, +} = require('~/server/services/ActionService'); const { findAccessibleResources } = require('~/server/services/PermissionService'); const { getAgent, updateAgent, getListAgentsByAccess } = require('~/models/Agent'); const { updateAction, getActions, deleteAction } = require('~/models/Action'); @@ -119,13 +123,14 @@ router.post( return res.status(400).json({ message: 'Domain not allowed' }); } - let { domain } = metadata; - domain = await domainParser(domain, true); + const encodedDomain = await domainParser(metadata.domain, true); - if (!domain) { + if (!encodedDomain) { return res.status(400).json({ message: 'No domain provided' }); } + const legacyDomain = legacyDomainEncode(metadata.domain); + const action_id = _action_id ?? nanoid(); const initialPromises = []; @@ -160,14 +165,23 @@ router.post( actions.push(action); } - actions.push(`${domain}${actionDelimiter}${action_id}`); + actions.push(`${encodedDomain}${actionDelimiter}${action_id}`); /** @type {string[]}} */ const { tools: _tools = [] } = agent; + const shouldRemoveAgentTool = (tool) => { + if (!tool) { + return false; + } + return ( + tool.includes(encodedDomain) || tool.includes(legacyDomain) || tool.includes(action_id) + ); + }; + const tools = _tools - .filter((tool) => !(tool && (tool.includes(domain) || tool.includes(action_id)))) - .concat(functions.map((tool) => `${tool.function.name}${actionDelimiter}${domain}`)); + .filter((tool) => !shouldRemoveAgentTool(tool)) + .concat(functions.map((tool) => `${tool.function.name}${actionDelimiter}${encodedDomain}`)); // Force version update since actions are changing const updatedAgent = await updateAgent( @@ -231,22 +245,22 @@ router.delete( const { tools = [], actions = [] } = agent; - let domain = ''; + let storedDomain = ''; const updatedActions = actions.filter((action) => { if (action.includes(action_id)) { - [domain] = action.split(actionDelimiter); + [storedDomain] = action.split(actionDelimiter); return false; } return true; }); - domain = await domainParser(domain, true); - - if (!domain) { + if (!storedDomain) { return res.status(400).json({ message: 'No domain provided' }); } - const updatedTools = tools.filter((tool) => !(tool && tool.includes(domain))); + const updatedTools = tools.filter( + (tool) => !(tool && (tool.includes(storedDomain) || tool.includes(action_id))), + ); // Force version update since actions are being removed await updateAgent( diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js index b085fbd36a..75ab879e2b 100644 --- a/api/server/routes/assistants/actions.js +++ b/api/server/routes/assistants/actions.js @@ -3,7 +3,11 @@ const { nanoid } = require('nanoid'); const { logger } = require('@librechat/data-schemas'); const { isActionDomainAllowed } = require('@librechat/api'); const { actionDelimiter, EModelEndpoint, removeNullishValues } = require('librechat-data-provider'); -const { encryptMetadata, domainParser } = require('~/server/services/ActionService'); +const { + legacyDomainEncode, + encryptMetadata, + domainParser, +} = require('~/server/services/ActionService'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { updateAction, getActions, deleteAction } = require('~/models/Action'); const { updateAssistantDoc, getAssistant } = require('~/models/Assistant'); @@ -39,13 +43,14 @@ router.post('/:assistant_id', async (req, res) => { return res.status(400).json({ message: 'Domain not allowed' }); } - let { domain } = metadata; - domain = await domainParser(domain, true); + const encodedDomain = await domainParser(metadata.domain, true); - if (!domain) { + if (!encodedDomain) { return res.status(400).json({ message: 'No domain provided' }); } + const legacyDomain = legacyDomainEncode(metadata.domain); + const action_id = _action_id ?? nanoid(); const initialPromises = []; @@ -81,25 +86,29 @@ router.post('/:assistant_id', async (req, res) => { actions.push(action); } - actions.push(`${domain}${actionDelimiter}${action_id}`); + actions.push(`${encodedDomain}${actionDelimiter}${action_id}`); /** @type {{ tools: FunctionTool[] | { type: 'code_interpreter'|'retrieval'}[]}} */ const { tools: _tools = [] } = assistant; + const shouldRemoveAssistantTool = (tool) => { + if (!tool.function) { + return false; + } + const name = tool.function.name; + return ( + name.includes(encodedDomain) || name.includes(legacyDomain) || name.includes(action_id) + ); + }; + const tools = _tools - .filter( - (tool) => - !( - tool.function && - (tool.function.name.includes(domain) || tool.function.name.includes(action_id)) - ), - ) + .filter((tool) => !shouldRemoveAssistantTool(tool)) .concat( functions.map((tool) => ({ ...tool, function: { ...tool.function, - name: `${tool.function.name}${actionDelimiter}${domain}`, + name: `${tool.function.name}${actionDelimiter}${encodedDomain}`, }, })), ); @@ -171,23 +180,25 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => { const { actions = [] } = assistant_data ?? {}; const { tools = [] } = assistant ?? {}; - let domain = ''; + let storedDomain = ''; const updatedActions = actions.filter((action) => { if (action.includes(action_id)) { - [domain] = action.split(actionDelimiter); + [storedDomain] = action.split(actionDelimiter); return false; } return true; }); - domain = await domainParser(domain, true); - - if (!domain) { + if (!storedDomain) { return res.status(400).json({ message: 'No domain provided' }); } const updatedTools = tools.filter( - (tool) => !(tool.function && tool.function.name.includes(domain)), + (tool) => + !( + tool.function && + (tool.function.name.includes(storedDomain) || tool.function.name.includes(action_id)) + ), ); await openai.beta.assistants.update(assistant_id, { tools: updatedTools }); diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index 5e96726a46..bde052bba4 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -28,6 +28,7 @@ const { getLogStores } = require('~/cache'); const JWT_SECRET = process.env.JWT_SECRET; const toolNameRegex = /^[a-zA-Z0-9_-]+$/; +const protocolRegex = /^https?:\/\//; const replaceSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); /** @@ -48,7 +49,11 @@ const validateAndUpdateTool = async ({ req, tool, assistant_id }) => { actions = await getActions({ assistant_id, user: req.user.id }, true); const matchingActions = actions.filter((action) => { const metadata = action.metadata; - return metadata && metadata.domain === domain; + if (!metadata) { + return false; + } + const strippedMetaDomain = stripProtocol(metadata.domain); + return strippedMetaDomain === domain || metadata.domain === domain; }); const action = matchingActions[0]; if (!action) { @@ -66,10 +71,36 @@ const validateAndUpdateTool = async ({ req, tool, assistant_id }) => { return tool; }; +/** @param {string} domain */ +function stripProtocol(domain) { + const stripped = domain.replace(protocolRegex, ''); + const pathIdx = stripped.indexOf('/'); + return pathIdx === -1 ? stripped : stripped.substring(0, pathIdx); +} + +/** + * Encodes a domain using the legacy scheme (full URL including protocol). + * Used for backward-compatible matching against agents saved before the collision fix. + * @param {string} domain + * @returns {string} + */ +function legacyDomainEncode(domain) { + if (!domain) { + return ''; + } + if (domain.length <= Constants.ENCODED_DOMAIN_LENGTH) { + return domain.replace(/\./g, actionDomainSeparator); + } + const modifiedDomain = Buffer.from(domain).toString('base64'); + return modifiedDomain.substring(0, Constants.ENCODED_DOMAIN_LENGTH); +} + /** * Encodes or decodes a domain name to/from base64, or replacing periods with a custom separator. * * Necessary due to `[a-zA-Z0-9_-]*` Regex Validation, limited to a 64-character maximum. + * Strips protocol prefix before encoding to prevent base64 collisions + * (all `https://` URLs share the same 10-char base64 prefix). * * @param {string} domain - The domain name to encode/decode. * @param {boolean} inverse - False to decode from base64, true to encode to base64. @@ -79,23 +110,27 @@ async function domainParser(domain, inverse = false) { if (!domain) { return; } - const domainsCache = getLogStores(CacheKeys.ENCODED_DOMAINS); - const cachedDomain = await domainsCache.get(domain); - if (inverse && cachedDomain) { - return domain; - } - if (inverse && domain.length <= Constants.ENCODED_DOMAIN_LENGTH) { - return domain.replace(/\./g, actionDomainSeparator); - } + const domainsCache = getLogStores(CacheKeys.ENCODED_DOMAINS); if (inverse) { - const modifiedDomain = Buffer.from(domain).toString('base64'); + const hostname = stripProtocol(domain); + const cachedDomain = await domainsCache.get(hostname); + if (cachedDomain) { + return hostname; + } + + if (hostname.length <= Constants.ENCODED_DOMAIN_LENGTH) { + return hostname.replace(/\./g, actionDomainSeparator); + } + + const modifiedDomain = Buffer.from(hostname).toString('base64'); const key = modifiedDomain.substring(0, Constants.ENCODED_DOMAIN_LENGTH); await domainsCache.set(key, modifiedDomain); return key; } + const cachedDomain = await domainsCache.get(domain); if (!cachedDomain) { return domain.replace(replaceSeparatorRegex, '.'); } @@ -456,6 +491,7 @@ const deleteAssistantActions = async ({ req, assistant_id }) => { module.exports = { deleteAssistantActions, validateAndUpdateTool, + legacyDomainEncode, createActionTool, encryptMetadata, decryptMetadata, diff --git a/api/server/services/ActionService.spec.js b/api/server/services/ActionService.spec.js index c60aef7ad1..42def44b4f 100644 --- a/api/server/services/ActionService.spec.js +++ b/api/server/services/ActionService.spec.js @@ -1,175 +1,539 @@ -const { Constants, actionDomainSeparator } = require('librechat-data-provider'); -const { domainParser } = require('./ActionService'); +const { Constants, actionDelimiter, actionDomainSeparator } = require('librechat-data-provider'); +const { domainParser, legacyDomainEncode, validateAndUpdateTool } = require('./ActionService'); jest.mock('keyv'); -const globalCache = {}; +jest.mock('~/models/Action', () => ({ + getActions: jest.fn(), + deleteActions: jest.fn(), +})); + +const { getActions } = require('~/models/Action'); + +let mockDomainCache = {}; jest.mock('~/cache/getLogStores', () => { - return jest.fn().mockImplementation(() => { - const EventEmitter = require('events'); - const { CacheKeys } = require('librechat-data-provider'); + return jest.fn().mockImplementation(() => ({ + get: async (key) => mockDomainCache[key] ?? null, + set: async (key, value) => { + mockDomainCache[key] = value; + return true; + }, + })); +}); - class KeyvMongo extends EventEmitter { - constructor(url = 'mongodb://127.0.0.1:27017', options) { - super(); - this.ttlSupport = false; - url = url ?? {}; - if (typeof url === 'string') { - url = { url }; - } - if (url.uri) { - url = { url: url.uri, ...url }; - } - this.opts = { - url, - collection: 'keyv', - ...url, - ...options, - }; - } +beforeEach(() => { + mockDomainCache = {}; + getActions.mockReset(); +}); - get = async (key) => { - return new Promise((resolve) => { - resolve(globalCache[key] || null); - }); - }; +const SEP = actionDomainSeparator; +const DELIM = actionDelimiter; +const MAX = Constants.ENCODED_DOMAIN_LENGTH; +const domainSepRegex = new RegExp(SEP, 'g'); - set = async (key, value) => { - return new Promise((resolve) => { - globalCache[key] = value; - resolve(true); - }); - }; - } +describe('domainParser', () => { + describe('nullish input', () => { + it.each([null, undefined, ''])('returns undefined for %j', async (input) => { + expect(await domainParser(input, true)).toBeUndefined(); + expect(await domainParser(input, false)).toBeUndefined(); + }); + }); - return new KeyvMongo('', { - namespace: CacheKeys.ENCODED_DOMAINS, - ttl: 0, + describe('short-path encoding (hostname ≤ threshold)', () => { + it.each([ + ['examp.com', `examp${SEP}com`], + ['swapi.tech', `swapi${SEP}tech`], + ['a.b', `a${SEP}b`], + ])('replaces dots in %s → %s', async (domain, expected) => { + expect(await domainParser(domain, true)).toBe(expected); + }); + + it('handles domain exactly at threshold length', async () => { + const domain = 'a'.repeat(MAX - 4) + '.com'; + expect(domain).toHaveLength(MAX); + const result = await domainParser(domain, true); + expect(result).toBe(domain.replace(/\./g, SEP)); + }); + }); + + describe('base64-path encoding (hostname > threshold)', () => { + it('produces a key of exactly ENCODED_DOMAIN_LENGTH chars', async () => { + const result = await domainParser('api.example.com', true); + expect(result).toHaveLength(MAX); + }); + + it('encodes hostname, not full URL', async () => { + const hostname = 'api.example.com'; + const expectedKey = Buffer.from(hostname).toString('base64').substring(0, MAX); + expect(await domainParser(hostname, true)).toBe(expectedKey); + }); + + it('populates decode cache for round-trip', async () => { + const hostname = 'longdomainname.com'; + const key = await domainParser(hostname, true); + + expect(mockDomainCache[key]).toBe(Buffer.from(hostname).toString('base64')); + expect(await domainParser(key, false)).toBe(hostname); + }); + }); + + describe('protocol stripping', () => { + it('https:// URL and bare hostname produce identical encoding', async () => { + const encoded = await domainParser('https://swapi.tech', true); + expect(encoded).toBe(await domainParser('swapi.tech', true)); + expect(encoded).toBe(`swapi${SEP}tech`); + }); + + it('http:// URL and bare hostname produce identical encoding', async () => { + const encoded = await domainParser('http://api.example.com', true); + expect(encoded).toBe(await domainParser('api.example.com', true)); + }); + + it('different https:// domains produce unique keys', async () => { + const keys = await Promise.all([ + domainParser('https://api.example.com', true), + domainParser('https://api.weather.com', true), + domainParser('https://data.github.com', true), + ]); + const unique = new Set(keys); + expect(unique.size).toBe(keys.length); + }); + + it('long hostname after stripping still uses base64 path', async () => { + const result = await domainParser('https://api.example.com', true); + expect(result).toHaveLength(MAX); + expect(result).not.toContain(SEP); + }); + + it('short hostname after stripping uses dot-replacement path', async () => { + const result = await domainParser('https://a.b.c', true); + expect(result).toBe(`a${SEP}b${SEP}c`); + }); + + it('strips path and query from full URL before encoding', async () => { + const result = await domainParser('https://api.example.com/v1/endpoint?foo=bar', true); + expect(result).toBe(await domainParser('api.example.com', true)); + }); + }); + + describe('unicode domains', () => { + it('encodes unicode hostname via base64 path', async () => { + const domain = 'täst.example.com'; + const result = await domainParser(domain, true); + expect(result).toHaveLength(MAX); + expect(result).toBe(Buffer.from(domain).toString('base64').substring(0, MAX)); + }); + + it('round-trips unicode hostname through encode then decode', async () => { + const domain = 'täst.example.com'; + const key = await domainParser(domain, true); + expect(await domainParser(key, false)).toBe(domain); + }); + + it('strips protocol before encoding unicode hostname', async () => { + const withProto = 'https://täst.example.com'; + const bare = 'täst.example.com'; + expect(await domainParser(withProto, true)).toBe(await domainParser(bare, true)); + }); + }); + + describe('decode path', () => { + it('short-path encoded domain decodes via separator replacement', async () => { + expect(await domainParser(`examp${SEP}com`, false)).toBe('examp.com'); + }); + + it('base64-path encoded domain decodes via cache lookup', async () => { + const hostname = 'api.example.com'; + const key = await domainParser(hostname, true); + expect(await domainParser(key, false)).toBe(hostname); + }); + + it('returns input unchanged for unknown non-separator strings', async () => { + expect(await domainParser('not_base64_encoded', false)).toBe('not_base64_encoded'); + }); + + it('returns a string without throwing for corrupt cache entries', async () => { + mockDomainCache['corrupt_key'] = '!!!'; + const result = await domainParser('corrupt_key', false); + expect(typeof result).toBe('string'); }); }); }); -describe('domainParser', () => { - const TLD = '.com'; - - // Non-azure request - it('does not return domain as is if not azure', async () => { - const domain = `example.com${actionDomainSeparator}test${actionDomainSeparator}`; - const result1 = await domainParser(domain, false); - const result2 = await domainParser(domain, true); - expect(result1).not.toEqual(domain); - expect(result2).not.toEqual(domain); +describe('legacyDomainEncode', () => { + it.each(['', null, undefined])('returns empty string for %j', (input) => { + expect(legacyDomainEncode(input)).toBe(''); }); - // Test for Empty or Null Inputs - it('returns undefined for null domain input', async () => { - const result = await domainParser(null, true); - expect(result).toBeUndefined(); + it('is synchronous (returns a string, not a Promise)', () => { + const result = legacyDomainEncode('examp.com'); + expect(result).toBe(`examp${SEP}com`); + expect(result).not.toBeInstanceOf(Promise); }); - it('returns undefined for empty domain input', async () => { - const result = await domainParser('', true); - expect(result).toBeUndefined(); + it('uses dot-replacement for short domains', () => { + expect(legacyDomainEncode('examp.com')).toBe(`examp${SEP}com`); }); - // Verify Correct Caching Behavior - it('caches encoded domain correctly', async () => { - const domain = 'longdomainname.com'; - const encodedDomain = Buffer.from(domain) - .toString('base64') - .substring(0, Constants.ENCODED_DOMAIN_LENGTH); - - await domainParser(domain, true); - - const cachedValue = await globalCache[encodedDomain]; - expect(cachedValue).toEqual(Buffer.from(domain).toString('base64')); + it('uses base64 prefix of full input for long domains', () => { + const domain = 'https://swapi.tech'; + const expected = Buffer.from(domain).toString('base64').substring(0, MAX); + expect(legacyDomainEncode(domain)).toBe(expected); }); - // Test for Edge Cases Around Length Threshold - it('encodes domain exactly at threshold without modification', async () => { - const domain = 'a'.repeat(Constants.ENCODED_DOMAIN_LENGTH - TLD.length) + TLD; - const expected = domain.replace(/\./g, actionDomainSeparator); - const result = await domainParser(domain, true); - expect(result).toEqual(expected); + it('all https:// URLs collide to the same key', () => { + const results = [ + legacyDomainEncode('https://api.example.com'), + legacyDomainEncode('https://api.weather.com'), + legacyDomainEncode('https://totally.different.host'), + ]; + expect(new Set(results).size).toBe(1); }); - it('encodes domain just below threshold without modification', async () => { - const domain = 'a'.repeat(Constants.ENCODED_DOMAIN_LENGTH - 1 - TLD.length) + TLD; - const expected = domain.replace(/\./g, actionDomainSeparator); - const result = await domainParser(domain, true); - expect(result).toEqual(expected); + it('matches what old domainParser would have produced', () => { + const domain = 'https://api.example.com'; + const legacy = legacyDomainEncode(domain); + expect(legacy).toBe(Buffer.from(domain).toString('base64').substring(0, MAX)); }); - // Test for Unicode Domain Names - it('handles unicode characters in domain names correctly when encoding', async () => { - const unicodeDomain = 'täst.example.com'; - const encodedDomain = Buffer.from(unicodeDomain) - .toString('base64') - .substring(0, Constants.ENCODED_DOMAIN_LENGTH); - const result = await domainParser(unicodeDomain, true); - expect(result).toEqual(encodedDomain); - }); - - it('decodes unicode domain names correctly', async () => { - const unicodeDomain = 'täst.example.com'; - const encodedDomain = Buffer.from(unicodeDomain).toString('base64'); - globalCache[encodedDomain.substring(0, Constants.ENCODED_DOMAIN_LENGTH)] = encodedDomain; // Simulate caching - - const result = await domainParser( - encodedDomain.substring(0, Constants.ENCODED_DOMAIN_LENGTH), - false, - ); - expect(result).toEqual(unicodeDomain); - }); - - // Core Functionality Tests - it('returns domain with replaced separators if no cached domain exists', async () => { - const domain = 'example.com'; - const withSeparator = domain.replace(/\./g, actionDomainSeparator); - const result = await domainParser(withSeparator, false); - expect(result).toEqual(domain); - }); - - it('returns domain with replaced separators when inverse is false and under encoding length', async () => { - const domain = 'examp.com'; - const withSeparator = domain.replace(/\./g, actionDomainSeparator); - const result = await domainParser(withSeparator, false); - expect(result).toEqual(domain); - }); - - it('replaces periods with actionDomainSeparator when inverse is true and under encoding length', async () => { - const domain = 'examp.com'; - const expected = domain.replace(/\./g, actionDomainSeparator); - const result = await domainParser(domain, true); - expect(result).toEqual(expected); - }); - - it('encodes domain when length is above threshold and inverse is true', async () => { - const domain = 'a'.repeat(Constants.ENCODED_DOMAIN_LENGTH + 1).concat('.com'); - const result = await domainParser(domain, true); - expect(result).not.toEqual(domain); - expect(result.length).toBeLessThanOrEqual(Constants.ENCODED_DOMAIN_LENGTH); - }); - - it('returns encoded value if no encoded value is cached, and inverse is false', async () => { - const originalDomain = 'example.com'; - const encodedDomain = Buffer.from( - originalDomain.replace(/\./g, actionDomainSeparator), - ).toString('base64'); - const result = await domainParser(encodedDomain, false); - expect(result).toEqual(encodedDomain); - }); - - it('decodes encoded value if cached and encoded value is provided, and inverse is false', async () => { - const originalDomain = 'example.com'; - const encodedDomain = await domainParser(originalDomain, true); - const result = await domainParser(encodedDomain, false); - expect(result).toEqual(originalDomain); - }); - - it('handles invalid base64 encoded values gracefully', async () => { - const invalidBase64Domain = 'not_base64_encoded'; - const result = await domainParser(invalidBase64Domain, false); - expect(result).toEqual(invalidBase64Domain); + it('produces same result as new domainParser for short bare hostnames', async () => { + const domain = 'swapi.tech'; + expect(legacyDomainEncode(domain)).toBe(await domainParser(domain, true)); + }); +}); + +describe('validateAndUpdateTool', () => { + const mockReq = { user: { id: 'user123' } }; + + it('returns tool unchanged when name passes tool-name regex', async () => { + const tool = { function: { name: 'getPeople_action_swapi---tech' } }; + const result = await validateAndUpdateTool({ + req: mockReq, + tool, + assistant_id: 'asst_1', + }); + expect(result).toEqual(tool); + expect(getActions).not.toHaveBeenCalled(); + }); + + it('matches action when metadata.domain has https:// prefix and tool domain is bare hostname', async () => { + getActions.mockResolvedValue([{ metadata: { domain: 'https://api.example.com' } }]); + + const tool = { function: { name: `getPeople${DELIM}api.example.com` } }; + const result = await validateAndUpdateTool({ + req: mockReq, + tool, + assistant_id: 'asst_1', + }); + + expect(result).not.toBeNull(); + expect(result.function.name).toMatch(/^getPeople_action_/); + expect(result.function.name).not.toContain('.'); + }); + + it('matches action when metadata.domain has no protocol', async () => { + getActions.mockResolvedValue([{ metadata: { domain: 'api.example.com' } }]); + + const tool = { function: { name: `getPeople${DELIM}api.example.com` } }; + const result = await validateAndUpdateTool({ + req: mockReq, + tool, + assistant_id: 'asst_1', + }); + + expect(result).not.toBeNull(); + expect(result.function.name).toMatch(/^getPeople_action_/); + }); + + it('returns null when no action matches the domain', async () => { + getActions.mockResolvedValue([{ metadata: { domain: 'https://other.domain.com' } }]); + + const tool = { function: { name: `getPeople${DELIM}api.example.com` } }; + const result = await validateAndUpdateTool({ + req: mockReq, + tool, + assistant_id: 'asst_1', + }); + + expect(result).toBeNull(); + }); + + it('returns null when action has no metadata', async () => { + getActions.mockResolvedValue([{ metadata: null }]); + + const tool = { function: { name: `getPeople${DELIM}api.example.com` } }; + const result = await validateAndUpdateTool({ + req: mockReq, + tool, + assistant_id: 'asst_1', + }); + + expect(result).toBeNull(); + }); +}); + +describe('backward-compatible tool name matching', () => { + function normalizeToolName(name) { + return name.replace(domainSepRegex, '_'); + } + + function buildToolName(functionName, encodedDomain) { + return `${functionName}${DELIM}${encodedDomain}`; + } + + describe('definition-phase matching', () => { + it('new encoding matches agent tools stored with new encoding', async () => { + const metadataDomain = 'https://swapi.tech'; + const encoded = await domainParser(metadataDomain, true); + const normalized = normalizeToolName(encoded); + + const storedTool = buildToolName('getPeople', encoded); + const defToolName = `getPeople${DELIM}${normalized}`; + + expect(normalizeToolName(storedTool)).toBe(defToolName); + }); + + it('legacy encoding matches agent tools stored with legacy encoding', async () => { + const metadataDomain = 'https://swapi.tech'; + const legacy = legacyDomainEncode(metadataDomain); + const legacyNormalized = normalizeToolName(legacy); + + const storedTool = buildToolName('getPeople', legacy); + const legacyDefName = `getPeople${DELIM}${legacyNormalized}`; + + expect(normalizeToolName(storedTool)).toBe(legacyDefName); + }); + + it('new definition matches old stored tools via legacy fallback', async () => { + const metadataDomain = 'https://swapi.tech'; + const newDomain = await domainParser(metadataDomain, true); + const legacyDomain = legacyDomainEncode(metadataDomain); + const newNorm = normalizeToolName(newDomain); + const legacyNorm = normalizeToolName(legacyDomain); + + const oldStoredTool = buildToolName('getPeople', legacyDomain); + const newToolName = `getPeople${DELIM}${newNorm}`; + const legacyToolName = `getPeople${DELIM}${legacyNorm}`; + + const storedNormalized = normalizeToolName(oldStoredTool); + const hasMatch = storedNormalized === newToolName || storedNormalized === legacyToolName; + expect(hasMatch).toBe(true); + }); + + it('pre-normalized Set eliminates per-tool normalization', async () => { + const metadataDomain = 'https://api.example.com'; + const domain = await domainParser(metadataDomain, true); + const legacyDomain = legacyDomainEncode(metadataDomain); + const normalizedDomain = normalizeToolName(domain); + const legacyNormalized = normalizeToolName(legacyDomain); + + const storedTools = [ + buildToolName('getWeather', legacyDomain), + buildToolName('getForecast', domain), + ]; + + const preNormalized = new Set(storedTools.map((t) => normalizeToolName(t))); + + const toolName = `getWeather${DELIM}${normalizedDomain}`; + const legacyToolName = `getWeather${DELIM}${legacyNormalized}`; + expect(preNormalized.has(toolName) || preNormalized.has(legacyToolName)).toBe(true); + }); + }); + + describe('execution-phase tool lookup', () => { + it('model-called tool name resolves via normalizedToDomain map (new encoding)', async () => { + const metadataDomain = 'https://api.example.com'; + const domain = await domainParser(metadataDomain, true); + const normalized = normalizeToolName(domain); + + const normalizedToDomain = new Map(); + normalizedToDomain.set(normalized, domain); + + const modelToolName = `getWeather${DELIM}${normalized}`; + + let matched = ''; + for (const [norm, canonical] of normalizedToDomain.entries()) { + if (modelToolName.includes(norm)) { + matched = canonical; + break; + } + } + + expect(matched).toBe(domain); + + const functionName = modelToolName.replace(`${DELIM}${normalizeToolName(matched)}`, ''); + expect(functionName).toBe('getWeather'); + }); + + it('model-called tool name resolves via legacy entry in normalizedToDomain map', async () => { + const metadataDomain = 'https://api.example.com'; + const domain = await domainParser(metadataDomain, true); + const legacyDomain = legacyDomainEncode(metadataDomain); + const legacyNorm = normalizeToolName(legacyDomain); + + const normalizedToDomain = new Map(); + normalizedToDomain.set(normalizeToolName(domain), domain); + normalizedToDomain.set(legacyNorm, domain); + + const legacyModelToolName = `getWeather${DELIM}${legacyNorm}`; + + let matched = ''; + for (const [norm, canonical] of normalizedToDomain.entries()) { + if (legacyModelToolName.includes(norm)) { + matched = canonical; + break; + } + } + + expect(matched).toBe(domain); + }); + + it('legacy guard skips duplicate map entry for short bare hostnames', async () => { + const domain = 'swapi.tech'; + const newEncoding = await domainParser(domain, true); + const legacyEncoding = legacyDomainEncode(domain); + + expect(newEncoding).toBe(legacyEncoding); + + const normalizedToDomain = new Map(); + normalizedToDomain.set(newEncoding, newEncoding); + if (legacyEncoding !== newEncoding) { + normalizedToDomain.set(legacyEncoding, newEncoding); + } + expect(normalizedToDomain.size).toBe(1); + }); + }); + + describe('processRequiredActions matching (assistants path)', () => { + it('legacy tool from OpenAI matches via normalizedToDomain with both encodings', async () => { + const metadataDomain = 'https://swapi.tech'; + const domain = await domainParser(metadataDomain, true); + const legacyDomain = legacyDomainEncode(metadataDomain); + + const normalizedToDomain = new Map(); + normalizedToDomain.set(domain, domain); + if (legacyDomain !== domain) { + normalizedToDomain.set(legacyDomain, domain); + } + + const legacyToolName = buildToolName('getPeople', legacyDomain); + + let currentDomain = ''; + let matchedKey = ''; + for (const [key, canonical] of normalizedToDomain.entries()) { + if (legacyToolName.includes(key)) { + currentDomain = canonical; + matchedKey = key; + break; + } + } + + expect(currentDomain).toBe(domain); + expect(matchedKey).toBe(legacyDomain); + + const functionName = legacyToolName.replace(`${DELIM}${matchedKey}`, ''); + expect(functionName).toBe('getPeople'); + }); + + it('new tool name matches via the canonical domain key', async () => { + const metadataDomain = 'https://swapi.tech'; + const domain = await domainParser(metadataDomain, true); + const legacyDomain = legacyDomainEncode(metadataDomain); + + const normalizedToDomain = new Map(); + normalizedToDomain.set(domain, domain); + if (legacyDomain !== domain) { + normalizedToDomain.set(legacyDomain, domain); + } + + const newToolName = buildToolName('getPeople', domain); + + let currentDomain = ''; + let matchedKey = ''; + for (const [key, canonical] of normalizedToDomain.entries()) { + if (newToolName.includes(key)) { + currentDomain = canonical; + matchedKey = key; + break; + } + } + + expect(currentDomain).toBe(domain); + expect(matchedKey).toBe(domain); + + const functionName = newToolName.replace(`${DELIM}${matchedKey}`, ''); + expect(functionName).toBe('getPeople'); + }); + }); + + describe('save-route cleanup', () => { + it('tool filter removes tools matching new encoding', async () => { + const metadataDomain = 'https://swapi.tech'; + const domain = await domainParser(metadataDomain, true); + const legacyDomain = legacyDomainEncode(metadataDomain); + + const tools = [ + buildToolName('getPeople', domain), + buildToolName('unrelated', 'other---domain'), + ]; + + const filtered = tools.filter((t) => !t.includes(domain) && !t.includes(legacyDomain)); + + expect(filtered).toEqual([buildToolName('unrelated', 'other---domain')]); + }); + + it('tool filter removes tools matching legacy encoding', async () => { + const metadataDomain = 'https://swapi.tech'; + const domain = await domainParser(metadataDomain, true); + const legacyDomain = legacyDomainEncode(metadataDomain); + + const tools = [ + buildToolName('getPeople', legacyDomain), + buildToolName('unrelated', 'other---domain'), + ]; + + const filtered = tools.filter((t) => !t.includes(domain) && !t.includes(legacyDomain)); + + expect(filtered).toEqual([buildToolName('unrelated', 'other---domain')]); + }); + }); + + describe('delete-route domain extraction', () => { + it('domain extracted from actions array is usable as-is for tool filtering', async () => { + const metadataDomain = 'https://api.example.com'; + const domain = await domainParser(metadataDomain, true); + const actionId = 'abc123'; + const actionEntry = `${domain}${DELIM}${actionId}`; + + const [storedDomain] = actionEntry.split(DELIM); + expect(storedDomain).toBe(domain); + + const tools = [buildToolName('getWeather', domain), buildToolName('getPeople', 'other')]; + + const filtered = tools.filter((t) => !t.includes(storedDomain)); + expect(filtered).toEqual([buildToolName('getPeople', 'other')]); + }); + }); + + describe('multi-action agents (collision scenario)', () => { + it('two https:// actions now produce distinct tool names', async () => { + const domain1 = await domainParser('https://api.weather.com', true); + const domain2 = await domainParser('https://api.spacex.com', true); + + const tool1 = buildToolName('getData', domain1); + const tool2 = buildToolName('getData', domain2); + + expect(tool1).not.toBe(tool2); + }); + + it('two https:// actions used to collide in legacy encoding', () => { + const legacy1 = legacyDomainEncode('https://api.weather.com'); + const legacy2 = legacyDomainEncode('https://api.spacex.com'); + + const tool1 = buildToolName('getData', legacy1); + const tool2 = buildToolName('getData', legacy2); + + expect(tool1).toBe(tool2); + }); }); }); diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 5fc95e748d..ca75e7eb4f 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -42,6 +42,7 @@ const { } = require('librechat-data-provider'); const { createActionTool, + legacyDomainEncode, decryptMetadata, loadActionSets, domainParser, @@ -65,6 +66,8 @@ const { findPluginAuthsByKeys } = require('~/models'); const { getFlowStateManager } = require('~/config'); const { getLogStores } = require('~/cache'); +const domainSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); + /** * Resolves the set of enabled agent capabilities from endpoints config, * falling back to app-level or default capabilities for ephemeral agents. @@ -172,8 +175,7 @@ async function processRequiredActions(client, requiredActions) { const promises = []; - /** @type {Action[]} */ - let actionSets = []; + let actionSetsData = null; let isActionTool = false; const ActionToolMap = {}; const ActionBuildersMap = {}; @@ -259,9 +261,9 @@ async function processRequiredActions(client, requiredActions) { if (!tool) { // throw new Error(`Tool ${currentAction.tool} not found.`); - // Load all action sets once if not already loaded - if (!actionSets.length) { - actionSets = + if (!actionSetsData) { + /** @type {Action[]} */ + const actionSets = (await loadActionSets({ assistant_id: client.req.body.assistant_id, })) ?? []; @@ -269,11 +271,16 @@ async function processRequiredActions(client, requiredActions) { // Process all action sets once // Map domains to their processed action sets const processedDomains = new Map(); - const domainMap = new Map(); + const domainLookupMap = new Map(); for (const action of actionSets) { const domain = await domainParser(action.metadata.domain, true); - domainMap.set(domain, action); + domainLookupMap.set(domain, domain); + + const legacyDomain = legacyDomainEncode(action.metadata.domain); + if (legacyDomain !== domain) { + domainLookupMap.set(legacyDomain, domain); + } const isDomainAllowed = await isActionDomainAllowed( action.metadata.domain, @@ -328,27 +335,26 @@ async function processRequiredActions(client, requiredActions) { ActionBuildersMap[action.metadata.domain] = requestBuilders; } - // Update actionSets reference to use the domain map - actionSets = { domainMap, processedDomains }; + actionSetsData = { domainLookupMap, processedDomains }; } - // Find the matching domain for this tool let currentDomain = ''; - for (const domain of actionSets.domainMap.keys()) { - if (currentAction.tool.includes(domain)) { - currentDomain = domain; + let matchedKey = ''; + for (const [key, canonical] of actionSetsData.domainLookupMap.entries()) { + if (currentAction.tool.includes(key)) { + currentDomain = canonical; + matchedKey = key; break; } } - if (!currentDomain || !actionSets.processedDomains.has(currentDomain)) { - // TODO: try `function` if no action set is found - // throw new Error(`Tool ${currentAction.tool} not found.`); + if (!currentDomain || !actionSetsData.processedDomains.has(currentDomain)) { continue; } - const { action, requestBuilders, encrypted } = actionSets.processedDomains.get(currentDomain); - const functionName = currentAction.tool.replace(`${actionDelimiter}${currentDomain}`, ''); + const { action, requestBuilders, encrypted } = + actionSetsData.processedDomains.get(currentDomain); + const functionName = currentAction.tool.replace(`${actionDelimiter}${matchedKey}`, ''); const requestBuilder = requestBuilders[functionName]; if (!requestBuilder) { @@ -586,12 +592,17 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to const definitions = []; const allowedDomains = appConfig?.actions?.allowedDomains; - const domainSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); + const normalizedToolNames = new Set( + actionToolNames.map((n) => n.replace(domainSeparatorRegex, '_')), + ); for (const action of actionSets) { const domain = await domainParser(action.metadata.domain, true); const normalizedDomain = domain.replace(domainSeparatorRegex, '_'); + const legacyDomain = legacyDomainEncode(action.metadata.domain); + const legacyNormalized = legacyDomain.replace(domainSeparatorRegex, '_'); + const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain, allowedDomains); if (!isDomainAllowed) { logger.warn( @@ -611,7 +622,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to for (const sig of functionSignatures) { const toolName = `${sig.name}${actionDelimiter}${normalizedDomain}`; - if (!actionToolNames.some((name) => name.replace(domainSeparatorRegex, '_') === toolName)) { + const legacyToolName = `${sig.name}${actionDelimiter}${legacyNormalized}`; + if (!normalizedToolNames.has(toolName) && !normalizedToolNames.has(legacyToolName)) { continue; } @@ -990,15 +1002,17 @@ async function loadAgentTools({ }; } - // Process each action set once (validate spec, decrypt metadata) const processedActionSets = new Map(); - const domainMap = new Map(); + const domainLookupMap = new Map(); for (const action of actionSets) { const domain = await domainParser(action.metadata.domain, true); - domainMap.set(domain, action); + domainLookupMap.set(domain, domain); - // Check if domain is allowed (do this once per action set) + const legacyDomain = legacyDomainEncode(action.metadata.domain); + if (legacyDomain !== domain) { + domainLookupMap.set(legacyDomain, domain); + } const isDomainAllowed = await isActionDomainAllowed( action.metadata.domain, appConfig?.actions?.allowedDomains, @@ -1060,11 +1074,12 @@ async function loadAgentTools({ continue; } - // Find the matching domain for this tool let currentDomain = ''; - for (const domain of domainMap.keys()) { - if (toolName.includes(domain)) { - currentDomain = domain; + let matchedKey = ''; + for (const [key, canonical] of domainLookupMap.entries()) { + if (toolName.includes(key)) { + currentDomain = canonical; + matchedKey = key; break; } } @@ -1075,7 +1090,7 @@ async function loadAgentTools({ const { action, encrypted, zodSchemas, requestBuilders, functionSignatures } = processedActionSets.get(currentDomain); - const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, ''); + const functionName = toolName.replace(`${actionDelimiter}${matchedKey}`, ''); const functionSig = functionSignatures.find((sig) => sig.name === functionName); const requestBuilder = requestBuilders[functionName]; const zodSchema = zodSchemas[functionName]; @@ -1310,12 +1325,20 @@ async function loadActionToolsForExecution({ } const processedActionSets = new Map(); - const domainMap = new Map(); + /** Maps both new and legacy normalized domains to their canonical (new) domain key */ + const normalizedToDomain = new Map(); const allowedDomains = appConfig?.actions?.allowedDomains; for (const action of actionSets) { const domain = await domainParser(action.metadata.domain, true); - domainMap.set(domain, action); + const normalizedDomain = domain.replace(domainSeparatorRegex, '_'); + normalizedToDomain.set(normalizedDomain, domain); + + const legacyDomain = legacyDomainEncode(action.metadata.domain); + const legacyNormalized = legacyDomain.replace(domainSeparatorRegex, '_'); + if (legacyNormalized !== normalizedDomain) { + normalizedToDomain.set(legacyNormalized, domain); + } const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain, allowedDomains); if (!isDomainAllowed) { @@ -1364,16 +1387,15 @@ async function loadActionToolsForExecution({ functionSignatures, zodSchemas, encrypted, + legacyNormalized, }); } - const domainSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); for (const toolName of actionToolNames) { let currentDomain = ''; - for (const domain of domainMap.keys()) { - const normalizedDomain = domain.replace(domainSeparatorRegex, '_'); + for (const [normalizedDomain, canonicalDomain] of normalizedToDomain.entries()) { if (toolName.includes(normalizedDomain)) { - currentDomain = domain; + currentDomain = canonicalDomain; break; } } @@ -1382,7 +1404,7 @@ async function loadActionToolsForExecution({ continue; } - const { action, encrypted, zodSchemas, requestBuilders, functionSignatures } = + const { action, encrypted, zodSchemas, requestBuilders, functionSignatures, legacyNormalized } = processedActionSets.get(currentDomain); const normalizedDomain = currentDomain.replace(domainSeparatorRegex, '_'); const functionName = toolName.replace(`${actionDelimiter}${normalizedDomain}`, ''); @@ -1391,6 +1413,25 @@ async function loadActionToolsForExecution({ const zodSchema = zodSchemas[functionName]; if (!requestBuilder) { + const legacyFnName = toolName.replace(`${actionDelimiter}${legacyNormalized}`, ''); + if (legacyFnName !== toolName && requestBuilders[legacyFnName]) { + const legacyTool = await createActionTool({ + userId: req.user.id, + res, + action, + streamId, + encrypted, + requestBuilder: requestBuilders[legacyFnName], + zodSchema: zodSchemas[legacyFnName], + name: toolName, + description: + functionSignatures.find((sig) => sig.name === legacyFnName)?.description ?? '', + useSSRFProtection: !Array.isArray(allowedDomains) || allowedDomains.length === 0, + }); + if (legacyTool) { + loadedActionTools.push(legacyTool); + } + } continue; } From 0c378811f1f9fb3d6d5a2a5f380868beabdec1d1 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 17 Mar 2026 02:12:34 -0400 Subject: [PATCH 059/111] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix:=20Clear=20?= =?UTF-8?q?ModelSpec=20Display=20Fields=20When=20Navigating=20via=20Agent?= =?UTF-8?q?=20Share=20Link=20(#12274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract `specDisplayFieldReset` constant and `mergeQuerySettingsWithSpec` utility to `client/src/utils/endpoints.ts` as a single source of truth for spec display fields that must be cleared on non-spec transitions. - Clear `spec`, `iconURL`, `modelLabel`, and `greeting` from the merged preset in `ChatRoute.getNewConvoPreset()` when URL query parameters override the conversation without explicitly setting a spec. - Also clear `greeting` in the parallel cleanup in `useQueryParams.newQueryConvo` using the shared `specDisplayFieldReset` constant. - Guard the field reset on `specPreset != null` so null values aren't injected when no spec is configured. - Add comprehensive test coverage for the merge-and-clear logic. --- client/src/hooks/Input/useQueryParams.ts | 12 +- client/src/routes/ChatRoute.tsx | 26 +-- .../mergeQuerySettingsWithSpec.test.ts | 152 ++++++++++++++++++ client/src/utils/endpoints.ts | 24 +++ 4 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 client/src/utils/__tests__/mergeQuerySettingsWithSpec.test.ts diff --git a/client/src/hooks/Input/useQueryParams.ts b/client/src/hooks/Input/useQueryParams.ts index b29f408a3a..85b5d8838b 100644 --- a/client/src/hooks/Input/useQueryParams.ts +++ b/client/src/hooks/Input/useQueryParams.ts @@ -12,6 +12,7 @@ import type { import { clearModelForNonEphemeralAgent, removeUnavailableTools, + specDisplayFieldReset, processValidSettings, getModelSpecIconURL, getConvoSwitchLogic, @@ -128,13 +129,10 @@ export default function useQueryParams({ endpointsConfig, }); - let resetParams = {}; + const resetFields = newPreset.spec == null ? specDisplayFieldReset : {}; if (newPreset.spec == null) { - template.spec = null; - template.iconURL = null; - template.modelLabel = null; - resetParams = { spec: null, iconURL: null, modelLabel: null }; - newPreset = { ...newPreset, ...resetParams }; + Object.assign(template, specDisplayFieldReset); + newPreset = { ...newPreset, ...specDisplayFieldReset }; } // Sync agent_id from newPreset to template, then clear model if non-ephemeral agent @@ -152,7 +150,7 @@ export default function useQueryParams({ conversation: { ...(conversation ?? {}), endpointType: template.endpointType, - ...resetParams, + ...resetFields, }, preset: template, cleanOutput: newPreset.spec != null && newPreset.spec !== '', diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index dcb58c3f49..a17d349037 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -6,20 +6,21 @@ import { Constants, EModelEndpoint } from 'librechat-data-provider'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; import type { TPreset } from 'librechat-data-provider'; import { - useNewConvo, - useAppStartup, + mergeQuerySettingsWithSpec, + processValidSettings, + getDefaultModelSpec, + getModelSpecPreset, + isNotFoundError, + logger, +} from '~/utils'; +import { useAssistantListMap, useIdChangeEffect, + useAppStartup, + useNewConvo, useLocalize, } from '~/hooks'; import { useGetConvoIdQuery, useGetStartupConfig, useGetEndpointsQuery } from '~/data-provider'; -import { - getDefaultModelSpec, - getModelSpecPreset, - processValidSettings, - logger, - isNotFoundError, -} from '~/utils'; import { ToolCallsMapProvider } from '~/Providers'; import ChatView from '~/components/Chat/ChatView'; import { NotificationSeverity } from '~/common'; @@ -102,9 +103,10 @@ export default function ChatRoute() { }); const querySettings = processValidSettings(queryParams); - return Object.keys(querySettings).length > 0 - ? { ...specPreset, ...querySettings } - : specPreset; + if (Object.keys(querySettings).length > 0) { + return mergeQuerySettingsWithSpec(specPreset, querySettings); + } + return specPreset; }; if (isNewConvo && endpointsQuery.data && modelsQuery.data) { diff --git a/client/src/utils/__tests__/mergeQuerySettingsWithSpec.test.ts b/client/src/utils/__tests__/mergeQuerySettingsWithSpec.test.ts new file mode 100644 index 0000000000..76e104f62f --- /dev/null +++ b/client/src/utils/__tests__/mergeQuerySettingsWithSpec.test.ts @@ -0,0 +1,152 @@ +import { EModelEndpoint } from 'librechat-data-provider'; +import type { TPreset } from 'librechat-data-provider'; +import { mergeQuerySettingsWithSpec, specDisplayFieldReset } from '../endpoints'; + +describe('mergeQuerySettingsWithSpec', () => { + const specPreset: TPreset = { + endpoint: EModelEndpoint.openAI, + model: 'gpt-4', + spec: 'my-spec', + iconURL: 'https://example.com/icon.png', + modelLabel: 'My Custom GPT', + greeting: 'Hello from the spec!', + temperature: 0.7, + }; + + describe('when specPreset is active and query has no spec', () => { + it('clears all spec display fields for agent share links', () => { + const querySettings: TPreset = { + agent_id: 'agent_123', + endpoint: EModelEndpoint.agents, + }; + + const result = mergeQuerySettingsWithSpec(specPreset, querySettings); + + expect(result.agent_id).toBe('agent_123'); + expect(result.endpoint).toBe(EModelEndpoint.agents); + expect(result.spec).toBeNull(); + expect(result.iconURL).toBeNull(); + expect(result.modelLabel).toBeNull(); + expect(result.greeting).toBeUndefined(); + }); + + it('preserves non-display settings from the spec base', () => { + const querySettings: TPreset = { + agent_id: 'agent_123', + endpoint: EModelEndpoint.agents, + }; + + const result = mergeQuerySettingsWithSpec(specPreset, querySettings); + + expect(result.temperature).toBe(0.7); + }); + + it('clears spec display fields for assistant share links', () => { + const querySettings: TPreset = { + assistant_id: 'asst_abc', + endpoint: EModelEndpoint.assistants, + }; + + const result = mergeQuerySettingsWithSpec(specPreset, querySettings); + + expect(result.assistant_id).toBe('asst_abc'); + expect(result.endpoint).toBe(EModelEndpoint.assistants); + expect(result.spec).toBeNull(); + expect(result.iconURL).toBeNull(); + expect(result.modelLabel).toBeNull(); + expect(result.greeting).toBeUndefined(); + }); + + it('clears spec display fields for model override links', () => { + const querySettings: TPreset = { + model: 'claude-sonnet-4-20250514', + endpoint: EModelEndpoint.anthropic, + }; + + const result = mergeQuerySettingsWithSpec(specPreset, querySettings); + + expect(result.model).toBe('claude-sonnet-4-20250514'); + expect(result.endpoint).toBe(EModelEndpoint.anthropic); + expect(result.spec).toBeNull(); + expect(result.iconURL).toBeNull(); + expect(result.modelLabel).toBeNull(); + expect(result.greeting).toBeUndefined(); + }); + }); + + describe('when query explicitly sets a spec', () => { + it('preserves spec display fields from the base', () => { + const querySettings = { spec: 'other-spec' } as TPreset; + + const result = mergeQuerySettingsWithSpec(specPreset, querySettings); + + expect(result.spec).toBe('other-spec'); + expect(result.iconURL).toBe('https://example.com/icon.png'); + expect(result.modelLabel).toBe('My Custom GPT'); + expect(result.greeting).toBe('Hello from the spec!'); + }); + }); + + describe('when specPreset is undefined (no spec configured)', () => { + it('returns querySettings without injecting null display fields', () => { + const querySettings: TPreset = { + agent_id: 'agent_123', + endpoint: EModelEndpoint.agents, + }; + + const result = mergeQuerySettingsWithSpec(undefined, querySettings); + + expect(result.agent_id).toBe('agent_123'); + expect(result.endpoint).toBe(EModelEndpoint.agents); + expect(result).not.toHaveProperty('spec'); + expect(result).not.toHaveProperty('iconURL'); + expect(result).not.toHaveProperty('modelLabel'); + expect(result).not.toHaveProperty('greeting'); + }); + }); + + describe('when querySettings is empty', () => { + it('still clears spec display fields (no query params is not an explicit spec)', () => { + const result = mergeQuerySettingsWithSpec(specPreset, {} as TPreset); + + expect(result.spec).toBeNull(); + expect(result.iconURL).toBeNull(); + expect(result.modelLabel).toBeNull(); + expect(result.greeting).toBeUndefined(); + expect(result.endpoint).toBe(EModelEndpoint.openAI); + expect(result.model).toBe('gpt-4'); + expect(result.temperature).toBe(0.7); + }); + }); + + describe('query settings override spec values', () => { + it('overrides endpoint and model from spec', () => { + const querySettings: TPreset = { + endpoint: EModelEndpoint.anthropic, + model: 'claude-sonnet-4-20250514', + }; + + const result = mergeQuerySettingsWithSpec(specPreset, querySettings); + + expect(result.endpoint).toBe(EModelEndpoint.anthropic); + expect(result.model).toBe('claude-sonnet-4-20250514'); + expect(result.temperature).toBe(0.7); + expect(result.spec).toBeNull(); + }); + }); +}); + +describe('specDisplayFieldReset', () => { + it('contains all spec display fields that need clearing', () => { + expect(specDisplayFieldReset).toEqual({ + spec: null, + iconURL: null, + modelLabel: null, + greeting: undefined, + }); + }); + + it('has exactly 4 fields', () => { + expect(Object.keys(specDisplayFieldReset)).toHaveLength(4); + }); +}); diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index 33aa7a8525..a27f71b8e9 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -311,6 +311,30 @@ export function getModelSpecPreset(modelSpec?: t.TModelSpec) { }; } +/** Fields set by a model spec that should be cleared when switching to a non-spec conversation. */ +export const specDisplayFieldReset = { + spec: null as string | null, + iconURL: null as string | null, + modelLabel: null as string | null, + greeting: undefined as string | undefined, +}; + +/** + * Merges a spec preset base with URL query settings, clearing spec display fields + * when the query doesn't explicitly set a spec. Prevents spec contamination on + * agent/assistant share links. + */ +export function mergeQuerySettingsWithSpec( + specPreset: t.TPreset | undefined, + querySettings: t.TPreset, +): t.TPreset { + return { + ...specPreset, + ...querySettings, + ...(specPreset != null && querySettings.spec == null ? specDisplayFieldReset : {}), + }; +} + /** Gets the default spec iconURL by order or definition. * * First, the admin defined default, then last selected spec, followed by first spec From 68435cdcd06da5b694a01ccf719b022e7ae8cd2e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 17 Mar 2026 02:36:18 -0400 Subject: [PATCH 060/111] =?UTF-8?q?=F0=9F=A7=AF=20fix:=20Add=20Pre-Parse?= =?UTF-8?q?=20File=20Size=20Guard=20to=20Document=20Parser=20(#12275)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent memory exhaustion DoS by rejecting documents exceeding 15MB before reading them into memory, closing the gap between the 512MB upload limit and unbounded in-memory parsing. --- packages/api/src/files/documents/crud.spec.ts | 24 +++++++++ packages/api/src/files/documents/crud.ts | 51 +++++++++++++------ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/packages/api/src/files/documents/crud.spec.ts b/packages/api/src/files/documents/crud.spec.ts index f22693718a..f8b255dd5e 100644 --- a/packages/api/src/files/documents/crud.spec.ts +++ b/packages/api/src/files/documents/crud.spec.ts @@ -122,6 +122,30 @@ describe('Document Parser', () => { await expect(parseDocument({ file })).rejects.toThrow('No text found in document'); }); + test('parseDocument() rejects files exceeding the pre-parse size limit', async () => { + const file = { + originalname: 'oversized.docx', + path: path.join(__dirname, 'sample.docx'), + mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + size: 16 * 1024 * 1024, + } as Express.Multer.File; + + await expect(parseDocument({ file })).rejects.toThrow( + /exceeds the 15MB document parser limit \(16MB\)/, + ); + }); + + test('parseDocument() allows files exactly at the size limit boundary', async () => { + const file = { + originalname: 'sample.docx', + path: path.join(__dirname, 'sample.docx'), + mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + size: 15 * 1024 * 1024, + } as Express.Multer.File; + + await expect(parseDocument({ file })).resolves.toBeDefined(); + }); + test('parseDocument() parses empty xlsx with only sheet name', async () => { const file = { originalname: 'empty.xlsx', diff --git a/packages/api/src/files/documents/crud.ts b/packages/api/src/files/documents/crud.ts index ab16534b45..61c1956542 100644 --- a/packages/api/src/files/documents/crud.ts +++ b/packages/api/src/files/documents/crud.ts @@ -1,35 +1,39 @@ import * as fs from 'fs'; -import { excelMimeTypes, FileSources } from 'librechat-data-provider'; +import { megabyte, excelMimeTypes, FileSources } from 'librechat-data-provider'; import type { TextItem } from 'pdfjs-dist/types/src/display/api'; import type { MistralOCRUploadResult } from '~/types'; +type FileParseFn = (file: Express.Multer.File) => Promise; + +const DOCUMENT_PARSER_MAX_FILE_SIZE = 15 * megabyte; + /** * Parses an uploaded document and extracts its text content and metadata. * Handled types must stay in sync with `documentParserMimeTypes` from data-provider. * - * @throws {Error} if `file.mimetype` is not handled or no text is found. + * @throws {Error} if `file.mimetype` is not handled, file exceeds size limit, or no text is found. */ export async function parseDocument({ file, }: { file: Express.Multer.File; }): Promise { - let text: string; - if (file.mimetype === 'application/pdf') { - text = await pdfToText(file); - } else if ( - file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - ) { - text = await wordDocToText(file); - } else if ( - excelMimeTypes.test(file.mimetype) || - file.mimetype === 'application/vnd.oasis.opendocument.spreadsheet' - ) { - text = await excelSheetToText(file); - } else { + const parseFn = getParserForMimeType(file.mimetype); + if (!parseFn) { throw new Error(`Unsupported file type in document parser: ${file.mimetype}`); } + const fileSize = file.size ?? (file.path != null ? (await fs.promises.stat(file.path)).size : 0); + if (fileSize > DOCUMENT_PARSER_MAX_FILE_SIZE) { + const limitMB = DOCUMENT_PARSER_MAX_FILE_SIZE / megabyte; + const sizeMB = Math.ceil(fileSize / megabyte); + throw new Error( + `File "${file.originalname}" exceeds the ${limitMB}MB document parser limit (${sizeMB}MB).`, + ); + } + + const text = await parseFn(file); + if (!text?.trim()) { throw new Error('No text found in document'); } @@ -43,6 +47,23 @@ export async function parseDocument({ }; } +/** Maps a MIME type to its document parser function, or `undefined` if unsupported. */ +function getParserForMimeType(mimetype: string): FileParseFn | undefined { + if (mimetype === 'application/pdf') { + return pdfToText; + } + if (mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { + return wordDocToText; + } + if ( + excelMimeTypes.test(mimetype) || + mimetype === 'application/vnd.oasis.opendocument.spreadsheet' + ) { + return excelSheetToText; + } + return undefined; +} + /** Parses PDF, returns text inside. */ async function pdfToText(file: Express.Multer.File): Promise { // Imported inline so that Jest can test other routes without failing due to loading ESM From 2f09d29c71335fe7d45427ad87402fd51bc5e7b7 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 17 Mar 2026 02:46:11 -0400 Subject: [PATCH 061/111] =?UTF-8?q?=F0=9F=9B=82=20fix:=20Validate=20`types?= =?UTF-8?q?`=20Query=20Param=20in=20People=20Picker=20Access=20Middleware?= =?UTF-8?q?=20(#12276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛂 fix: Validate `types` query param in people picker access middleware checkPeoplePickerAccess only inspected `req.query.type` (singular), allowing callers to bypass type-specific permission checks by using the `types` (plural) parameter accepted by the controller. Now both `type` and `types` are collected and each requested principal type is validated against the caller's role permissions. * 🛂 refactor: Hoist valid types constant, improve logging, and add edge-case tests - Hoist VALID_PRINCIPAL_TYPES to module-level Set to avoid per-request allocation - Include both `type` and `types` in error log for debuggability - Restore detailed JSDoc documenting per-type permission requirements - Add missing .json() assertion on partial-denial test - Add edge-case tests: all-invalid types, empty string types, PrincipalType.PUBLIC * 🏷️ fix: Align TPrincipalSearchParams with actual controller API The stale type used `type` (singular) but the controller and all callers use `types` (plural array). Aligns with PrincipalSearchParams in types/queries.ts. --- .../middleware/checkPeoplePickerAccess.js | 54 ++++-- .../checkPeoplePickerAccess.spec.js | 167 +++++++++++++++++- .../data-provider/src/accessPermissions.ts | 8 +- 3 files changed, 209 insertions(+), 20 deletions(-) diff --git a/api/server/middleware/checkPeoplePickerAccess.js b/api/server/middleware/checkPeoplePickerAccess.js index 0e604272db..af2154dbba 100644 --- a/api/server/middleware/checkPeoplePickerAccess.js +++ b/api/server/middleware/checkPeoplePickerAccess.js @@ -2,13 +2,20 @@ const { logger } = require('@librechat/data-schemas'); const { PrincipalType, PermissionTypes, Permissions } = require('librechat-data-provider'); const { getRoleByName } = require('~/models/Role'); +const VALID_PRINCIPAL_TYPES = new Set([ + PrincipalType.USER, + PrincipalType.GROUP, + PrincipalType.ROLE, +]); + /** - * Middleware to check if user has permission to access people picker functionality - * Checks specific permission based on the 'type' query parameter: - * - type=user: requires VIEW_USERS permission - * - type=group: requires VIEW_GROUPS permission - * - type=role: requires VIEW_ROLES permission - * - no type (mixed search): requires either VIEW_USERS OR VIEW_GROUPS OR VIEW_ROLES + * Middleware to check if user has permission to access people picker functionality. + * Validates requested principal types via `type` (singular) and `types` (comma-separated or array) + * query parameters against the caller's role permissions: + * - user: requires VIEW_USERS permission + * - group: requires VIEW_GROUPS permission + * - role: requires VIEW_ROLES permission + * - no type filter (mixed search): requires at least one of the above */ const checkPeoplePickerAccess = async (req, res, next) => { try { @@ -28,7 +35,7 @@ const checkPeoplePickerAccess = async (req, res, next) => { }); } - const { type } = req.query; + const { type, types } = req.query; const peoplePickerPerms = role.permissions[PermissionTypes.PEOPLE_PICKER] || {}; const canViewUsers = peoplePickerPerms[Permissions.VIEW_USERS] === true; const canViewGroups = peoplePickerPerms[Permissions.VIEW_GROUPS] === true; @@ -49,15 +56,32 @@ const checkPeoplePickerAccess = async (req, res, next) => { }, }; - const check = permissionChecks[type]; - if (check && !check.hasPermission) { - return res.status(403).json({ - error: 'Forbidden', - message: check.message, - }); + const requestedTypes = new Set(); + + if (type && VALID_PRINCIPAL_TYPES.has(type)) { + requestedTypes.add(type); } - if (!type && !canViewUsers && !canViewGroups && !canViewRoles) { + if (types) { + const typesArray = Array.isArray(types) ? types : types.split(','); + for (const t of typesArray) { + if (VALID_PRINCIPAL_TYPES.has(t)) { + requestedTypes.add(t); + } + } + } + + for (const requested of requestedTypes) { + const check = permissionChecks[requested]; + if (!check.hasPermission) { + return res.status(403).json({ + error: 'Forbidden', + message: check.message, + }); + } + } + + if (requestedTypes.size === 0 && !canViewUsers && !canViewGroups && !canViewRoles) { return res.status(403).json({ error: 'Forbidden', message: 'Insufficient permissions to search for users, groups, or roles', @@ -67,7 +91,7 @@ const checkPeoplePickerAccess = async (req, res, next) => { next(); } catch (error) { logger.error( - `[checkPeoplePickerAccess][${req.user?.id}] checkPeoplePickerAccess error for req.query.type = ${req.query.type}`, + `[checkPeoplePickerAccess][${req.user?.id}] error for type=${req.query.type}, types=${req.query.types}`, error, ); return res.status(500).json({ diff --git a/api/server/middleware/checkPeoplePickerAccess.spec.js b/api/server/middleware/checkPeoplePickerAccess.spec.js index 52bf0e6724..9a229610de 100644 --- a/api/server/middleware/checkPeoplePickerAccess.spec.js +++ b/api/server/middleware/checkPeoplePickerAccess.spec.js @@ -173,6 +173,171 @@ describe('checkPeoplePickerAccess', () => { expect(next).not.toHaveBeenCalled(); }); + it('should deny access when using types param to bypass type-specific check', async () => { + req.query.types = PrincipalType.GROUP; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: 'Insufficient permissions to search for groups', + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('should deny access when types contains any unpermitted type', async () => { + req.query.types = `${PrincipalType.USER},${PrincipalType.ROLE}`; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: 'Insufficient permissions to search for roles', + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('should allow access when all requested types are permitted', async () => { + req.query.types = `${PrincipalType.USER},${PrincipalType.GROUP}`; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: true, + [Permissions.VIEW_ROLES]: false, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('should validate types when provided as array (Express qs parsing)', async () => { + req.query.types = [PrincipalType.GROUP, PrincipalType.ROLE]; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: true, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: 'Insufficient permissions to search for groups', + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('should enforce permissions for combined type and types params', async () => { + req.query.type = PrincipalType.USER; + req.query.types = PrincipalType.GROUP; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: 'Insufficient permissions to search for groups', + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('should treat all-invalid types values as mixed search', async () => { + req.query.types = 'foobar'; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('should deny when types is empty string and user has no permissions', async () => { + req.query.types = ''; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: 'Insufficient permissions to search for users, groups, or roles', + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('should treat types=public as mixed search since PUBLIC is not a searchable principal type', async () => { + req.query.types = PrincipalType.PUBLIC; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + }, + }); + + await checkPeoplePickerAccess(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + it('should allow mixed search when user has at least one permission', async () => { // No type specified = mixed search req.query.type = undefined; @@ -222,7 +387,7 @@ describe('checkPeoplePickerAccess', () => { await checkPeoplePickerAccess(req, res, next); expect(logger.error).toHaveBeenCalledWith( - '[checkPeoplePickerAccess][user123] checkPeoplePickerAccess error for req.query.type = undefined', + '[checkPeoplePickerAccess][user123] error for type=undefined, types=undefined', error, ); expect(res.status).toHaveBeenCalledWith(500); diff --git a/packages/data-provider/src/accessPermissions.ts b/packages/data-provider/src/accessPermissions.ts index f2431fcf9a..bc97458076 100644 --- a/packages/data-provider/src/accessPermissions.ts +++ b/packages/data-provider/src/accessPermissions.ts @@ -200,9 +200,9 @@ export type TUpdateResourcePermissionsResponse = z.infer< * Principal search request parameters */ export type TPrincipalSearchParams = { - q: string; // search query (required) - limit?: number; // max results (1-50, default 10) - type?: PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE; // filter by type (optional) + q: string; + limit?: number; + types?: Array; }; /** @@ -228,7 +228,7 @@ export type TPrincipalSearchResult = { export type TPrincipalSearchResponse = { query: string; limit: number; - type?: PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE; + types?: Array | null; results: TPrincipalSearchResult[]; count: number; sources: { From 5b31bb720d6deb6f386173161eb55efd09e4273d Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:50:18 -0700 Subject: [PATCH 062/111] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Proper=20MCP=20Me?= =?UTF-8?q?nu=20Dismissal=20(#12256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: replace manual focus hack with modal menu in MCP selector Use Ariakit's `modal={true}` instead of a manual `requestAnimationFrame` focus-restore wrapper, which eliminates the `useRef`/`useCallback` overhead and lets Ariakit manage focus trapping natively. Also removes the unused `focusLoop` option from both MCP menu stores and a narrating comment in MCPSubMenu. * test: add MCPSelect menu interaction tests Cover button rendering, menu open/close via click and Escape, and server toggle keeping the menu open. Renders real MCPServerMenuItem and StackedMCPIcons components instead of re-implementing their logic in mocks. * fix: add unmountOnHide to MCP menu for consistency Matches the pattern used by MCPSubMenu, BookmarkMenu, and other Ariakit menus in the codebase. Ensures the menu fully detaches from the DOM and accessibility tree when closed. * fix: restore focusLoop on MCP menu stores Ariakit's CompositeStore (which MenuStore extends) defaults focusLoop to false. The previous commit incorrectly removed the explicit focusLoop: true, which silently disabled Arrow-key wraparound (mandatory per WAI-ARIA Menu pattern). modal={true} only traps Tab focus — it does not enable Arrow-key looping. * test: improve MCPSelect test coverage and mock hygiene - Add aria-modal regression guard so removing modal={true} fails a test - Add guard branch tests: no MCP access, empty servers, unpinned+empty - Fix TooltipAnchor mock to correctly spread array children - Fix import ordering per project conventions - Move component import to top with other imports - Replace unasserted jest.fn() mocks with plain values - Use mutable module-scoped vars for per-test mock overrides * fix: enhance pointer event handling in FavoriteItem component Updated the opacity and pointer events logic in the FavoriteItem component to improve user interaction. The changes ensure that the component correctly manages pointer events based on the popover state, enhancing accessibility and usability. * test: add MCPSubMenu menu interaction tests Cover guard branch (empty servers), submenu open/close with real Ariakit components and real MCPServerMenuItem, toggle persistence, pin/unpin button behavior and aria-label states. Only context providers and cross-package UI are mocked. * test: add focusLoop regression guard for both MCP menus ArrowDown from the last item must wrap to the first — this fails without focusLoop: true on the menu store, directly guarding the keyboard accessibility regression that was silently introduced. --------- Co-authored-by: Danny Avila --- .../src/components/Chat/Input/MCPSelect.tsx | 21 +-- .../src/components/Chat/Input/MCPSubMenu.tsx | 1 - .../Chat/Input/__tests__/MCPSelect.spec.tsx | 142 ++++++++++++++++ .../Chat/Input/__tests__/MCPSubMenu.spec.tsx | 156 ++++++++++++++++++ .../components/Nav/Favorites/FavoriteItem.tsx | 4 +- 5 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx create mode 100644 client/src/components/Chat/Input/__tests__/MCPSubMenu.spec.tsx diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index a5356f5094..13a86c856a 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo, useCallback, useRef } from 'react'; +import React, { memo, useMemo } from 'react'; import * as Ariakit from '@ariakit/react'; import { ChevronDown } from 'lucide-react'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; @@ -27,24 +27,9 @@ function MCPSelectContent() { const menuStore = Ariakit.useMenuStore({ focusLoop: true }); const isOpen = menuStore.useState('open'); - const focusedElementRef = useRef(null); const selectedCount = mcpValues?.length ?? 0; - // Wrap toggleServerSelection to preserve focus after state update - const handleToggle = useCallback( - (serverName: string) => { - // Save currently focused element - focusedElementRef.current = document.activeElement as HTMLElement; - toggleServerSelection(serverName); - // Restore focus after React re-renders - requestAnimationFrame(() => { - focusedElementRef.current?.focus(); - }); - }, - [toggleServerSelection], - ); - const selectedServers = useMemo(() => { if (!mcpValues || mcpValues.length === 0) { return []; @@ -103,6 +88,8 @@ function MCPSelectContent() { ))}
diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index b0b8fad1bb..f8e617cba3 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -35,7 +35,6 @@ const MCPSubMenu = React.forwardRef( placement: 'right', }); - // Don't render if no MCP servers are configured if (!selectableServers || selectableServers.length === 0) { return null; } diff --git a/client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx b/client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx new file mode 100644 index 0000000000..7662ee5e6e --- /dev/null +++ b/client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen, within } from '@testing-library/react'; +import MCPSelect from '../MCPSelect'; + +const mockToggleServerSelection = jest.fn(); + +const defaultMcpServerManager = { + localize: (key: string) => key, + isPinned: true, + mcpValues: [] as string[], + placeholderText: 'MCP Servers', + selectableServers: [ + { serverName: 'server-a', config: { title: 'Server A' } }, + { serverName: 'server-b', config: { title: 'Server B' } }, + ], + connectionStatus: {}, + isInitializing: () => false, + getConfigDialogProps: () => null, + toggleServerSelection: mockToggleServerSelection, + getServerStatusIconProps: () => null, +}; + +let mockCanUseMcp = true; +let mockMcpServerManager = { ...defaultMcpServerManager }; + +jest.mock('~/Providers', () => ({ + useBadgeRowContext: () => ({ + conversationId: 'test-conv', + storageContextKey: undefined, + mcpServerManager: mockMcpServerManager, + }), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, + useHasAccess: () => mockCanUseMcp, +})); + +jest.mock('@librechat/client', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const R = require('react'); + return { + TooltipAnchor: ({ + children, + render, + }: { + children: React.ReactNode; + render: React.ReactElement; + }) => R.cloneElement(render, {}, ...(Array.isArray(children) ? children : [children])), + MCPIcon: ({ className }: { className?: string }) => R.createElement('span', { className }), + Spinner: ({ className }: { className?: string }) => R.createElement('span', { className }), + }; +}); + +jest.mock('~/components/MCP/MCPConfigDialog', () => ({ + __esModule: true, + default: () => null, +})); + +describe('MCPSelect', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockCanUseMcp = true; + mockMcpServerManager = { ...defaultMcpServerManager }; + }); + + it('renders the menu button', () => { + render(); + expect(screen.getByRole('button', { name: /MCP Servers/i })).toBeInTheDocument(); + }); + + it('opens menu on button click and shows server items', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: /MCP Servers/i })); + + const menu = screen.getByRole('menu', { name: /com_ui_mcp_servers/i }); + expect(menu).toBeVisible(); + expect(within(menu).getByRole('menuitemcheckbox', { name: /Server A/i })).toBeInTheDocument(); + expect(within(menu).getByRole('menuitemcheckbox', { name: /Server B/i })).toBeInTheDocument(); + }); + + it('closes menu on Escape', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: /MCP Servers/i })); + expect(screen.getByRole('menu', { name: /com_ui_mcp_servers/i })).toBeVisible(); + + await user.keyboard('{Escape}'); + expect(screen.getByRole('button', { name: /MCP Servers/i })).toHaveAttribute( + 'aria-expanded', + 'false', + ); + }); + + it('keeps menu open after toggling a server item', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: /MCP Servers/i })); + await user.click(screen.getByRole('menuitemcheckbox', { name: /Server A/i })); + + expect(mockToggleServerSelection).toHaveBeenCalledWith('server-a'); + expect(screen.getByRole('menu', { name: /com_ui_mcp_servers/i })).toBeVisible(); + }); + + it('arrow-key navigation wraps from last item to first', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: /MCP Servers/i })); + const items = screen.getAllByRole('menuitemcheckbox'); + expect(items).toHaveLength(2); + + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowDown}'); + expect(items[0]).toHaveFocus(); + }); + + it('renders nothing when user lacks MCP access', () => { + mockCanUseMcp = false; + const { container } = render(); + expect(container.firstChild).toBeNull(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('renders nothing when selectableServers is empty', () => { + mockMcpServerManager = { ...defaultMcpServerManager, selectableServers: [] }; + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('renders nothing when not pinned and no servers selected', () => { + mockMcpServerManager = { ...defaultMcpServerManager, isPinned: false, mcpValues: [] }; + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/client/src/components/Chat/Input/__tests__/MCPSubMenu.spec.tsx b/client/src/components/Chat/Input/__tests__/MCPSubMenu.spec.tsx new file mode 100644 index 0000000000..be8fb5d9c2 --- /dev/null +++ b/client/src/components/Chat/Input/__tests__/MCPSubMenu.spec.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import * as Ariakit from '@ariakit/react'; +import userEvent from '@testing-library/user-event'; +import { render, screen, within } from '@testing-library/react'; +import MCPSubMenu from '../MCPSubMenu'; + +const mockToggleServerSelection = jest.fn(); +const mockSetIsPinned = jest.fn(); + +const defaultMcpServerManager = { + isPinned: true, + mcpValues: [] as string[], + setIsPinned: mockSetIsPinned, + placeholderText: 'MCP Servers', + selectableServers: [ + { serverName: 'server-a', config: { title: 'Server A' } }, + { serverName: 'server-b', config: { title: 'Server B', description: 'Second server' } }, + ], + connectionStatus: {}, + isInitializing: () => false, + getConfigDialogProps: () => null, + toggleServerSelection: mockToggleServerSelection, + getServerStatusIconProps: () => null, +}; + +let mockMcpServerManager = { ...defaultMcpServerManager }; + +jest.mock('~/Providers', () => ({ + useBadgeRowContext: () => ({ + storageContextKey: undefined, + mcpServerManager: mockMcpServerManager, + }), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, + useHasAccess: () => true, +})); + +jest.mock('@librechat/client', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const R = require('react'); + return { + MCPIcon: ({ className }: { className?: string }) => R.createElement('span', { className }), + PinIcon: ({ unpin }: { unpin?: boolean }) => + R.createElement('span', { 'data-testid': unpin ? 'unpin-icon' : 'pin-icon' }), + Spinner: ({ className }: { className?: string }) => R.createElement('span', { className }), + }; +}); + +jest.mock('~/components/MCP/MCPConfigDialog', () => ({ + __esModule: true, + default: () => null, +})); + +function ParentMenu({ children }: { children: React.ReactNode }) { + return ( + + {/* eslint-disable-next-line i18next/no-literal-string */} + Parent + {children} + + ); +} + +function renderSubMenu(props: React.ComponentProps = {}) { + return render( + + + , + ); +} + +describe('MCPSubMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockMcpServerManager = { ...defaultMcpServerManager }; + }); + + it('renders nothing when selectableServers is empty', () => { + mockMcpServerManager = { ...defaultMcpServerManager, selectableServers: [] }; + renderSubMenu(); + expect(screen.queryByText('MCP Servers')).not.toBeInTheDocument(); + }); + + it('renders the submenu trigger with default placeholder', () => { + renderSubMenu(); + expect(screen.getByText('MCP Servers')).toBeInTheDocument(); + }); + + it('renders custom placeholder when provided', () => { + renderSubMenu({ placeholder: 'Custom Label' }); + expect(screen.getByText('Custom Label')).toBeInTheDocument(); + expect(screen.queryByText('MCP Servers')).not.toBeInTheDocument(); + }); + + it('opens submenu and shows real server items', async () => { + const user = userEvent.setup(); + renderSubMenu(); + + await user.click(screen.getByText('MCP Servers')); + + const menu = screen.getByRole('menu', { name: /com_ui_mcp_servers/i }); + expect(menu).toBeVisible(); + expect(within(menu).getByRole('menuitemcheckbox', { name: /Server A/i })).toBeInTheDocument(); + expect(within(menu).getByRole('menuitemcheckbox', { name: /Server B/i })).toBeInTheDocument(); + }); + + it('keeps menu open after toggling a server item', async () => { + const user = userEvent.setup(); + renderSubMenu(); + + await user.click(screen.getByText('MCP Servers')); + await user.click(screen.getByRole('menuitemcheckbox', { name: /Server A/i })); + + expect(mockToggleServerSelection).toHaveBeenCalledWith('server-a'); + expect(screen.getByRole('menu', { name: /com_ui_mcp_servers/i })).toBeVisible(); + }); + + it('calls setIsPinned with toggled value when pin button is clicked', async () => { + const user = userEvent.setup(); + mockMcpServerManager = { ...defaultMcpServerManager, isPinned: false }; + renderSubMenu(); + + await user.click(screen.getByRole('button', { name: /com_ui_pin/i })); + + expect(mockSetIsPinned).toHaveBeenCalledWith(true); + }); + + it('arrow-key navigation wraps from last item to first', async () => { + const user = userEvent.setup(); + renderSubMenu(); + + await user.click(screen.getByText('MCP Servers')); + const items = screen.getAllByRole('menuitemcheckbox'); + expect(items).toHaveLength(2); + + await user.click(items[1]); + expect(items[1]).toHaveFocus(); + + await user.keyboard('{ArrowDown}'); + expect(items[0]).toHaveFocus(); + }); + + it('pin button shows unpin label when pinned', () => { + mockMcpServerManager = { ...defaultMcpServerManager, isPinned: true }; + renderSubMenu(); + expect(screen.getByRole('button', { name: /com_ui_unpin/i })).toBeInTheDocument(); + }); + + it('pin button shows pin label when not pinned', () => { + mockMcpServerManager = { ...defaultMcpServerManager, isPinned: false }; + renderSubMenu(); + expect(screen.getByRole('button', { name: /com_ui_pin/i })).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/Nav/Favorites/FavoriteItem.tsx b/client/src/components/Nav/Favorites/FavoriteItem.tsx index 173be27d00..248008869d 100644 --- a/client/src/components/Nav/Favorites/FavoriteItem.tsx +++ b/client/src/components/Nav/Favorites/FavoriteItem.tsx @@ -126,8 +126,8 @@ export default function FavoriteItem({ className={cn( 'absolute right-2 flex items-center', isPopoverActive - ? 'opacity-100' - : 'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100', + ? 'pointer-events-auto opacity-100' + : 'pointer-events-none opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100', )} onClick={(e) => e.stopPropagation()} > From 1e1a3a8f8df1f44d66121fe02f2718ca5d548117 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 17 Mar 2026 15:50:36 -0400 Subject: [PATCH 063/111] =?UTF-8?q?=E2=9C=A8=20v0.8.4-rc1=20(#12285)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App version: v0.8.3 → v0.8.4-rc1 - @librechat/api: 1.7.25 → 1.7.26 - @librechat/client: 0.4.54 → 0.4.55 - librechat-data-provider: 0.8.302 → 0.8.400 - @librechat/data-schemas: 0.0.38 → 0.0.39 --- Dockerfile | 2 +- Dockerfile.multi | 2 +- api/package.json | 2 +- bun.lock | 418 +++++++++++++++------------ client/jest.config.cjs | 2 +- client/package.json | 2 +- e2e/jestSetup.js | 2 +- helm/librechat/Chart.yaml | 4 +- package-lock.json | 16 +- package.json | 2 +- packages/api/package.json | 2 +- packages/client/package.json | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/config.ts | 2 +- packages/data-schemas/package.json | 2 +- 15 files changed, 263 insertions(+), 199 deletions(-) diff --git a/Dockerfile b/Dockerfile index bbff8133da..02bda8a589 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# v0.8.3 +# v0.8.4-rc1 # Base node image FROM node:20-alpine AS node diff --git a/Dockerfile.multi b/Dockerfile.multi index 53810b5f0a..8e7483e378 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -1,5 +1,5 @@ # Dockerfile.multi -# v0.8.3 +# v0.8.4-rc1 # Set configurable max-old-space-size with default ARG NODE_MAX_OLD_SPACE_SIZE=6144 diff --git a/api/package.json b/api/package.json index 89a5183ddd..f32de5e778 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/backend", - "version": "v0.8.3", + "version": "v0.8.4-rc1", "description": "", "scripts": { "start": "echo 'please run this from the root directory'", diff --git a/bun.lock b/bun.lock index 39d9641ec4..f6e3228519 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "api": { "name": "@librechat/backend", - "version": "0.8.3", + "version": "0.8.4-rc1", "dependencies": { "@anthropic-ai/vertex-sdk": "^0.14.3", "@aws-sdk/client-bedrock-runtime": "^3.980.0", @@ -49,13 +49,14 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.55", + "@librechat/agents": "^3.1.56", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", "@modelcontextprotocol/sdk": "^1.27.1", "@node-saml/passport-saml": "^5.1.0", "@smithy/node-http-handler": "^4.4.5", + "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "bcryptjs": "^2.4.3", "compression": "^1.8.1", @@ -71,7 +72,7 @@ "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", - "file-type": "^18.7.0", + "file-type": "^21.3.2", "firebase": "^11.0.2", "form-data": "^4.0.4", "handlebars": "^4.7.7", @@ -111,10 +112,9 @@ "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "sharp": "^0.33.5", - "tiktoken": "^1.0.15", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "undici": "^7.18.2", + "undici": "^7.24.1", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", @@ -129,7 +129,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "0.8.3", + "version": "0.8.4-rc1", "dependencies": { "@ariakit/react": "^0.4.15", "@ariakit/react-core": "^0.4.17", @@ -263,7 +263,7 @@ }, "packages/api": { "name": "@librechat/api", - "version": "1.7.25", + "version": "1.7.26", "devDependencies": { "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", @@ -307,10 +307,11 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.55", + "@librechat/agents": "^3.1.56", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", + "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "connect-redis": "^8.1.0", "eventsource": "^3.0.2", @@ -333,14 +334,13 @@ "node-fetch": "2.7.0", "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", - "tiktoken": "^1.0.15", - "undici": "^7.18.2", + "undici": "^7.24.1", "zod": "^3.22.4", }, }, "packages/client": { "name": "@librechat/client", - "version": "0.4.54", + "version": "0.4.55", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -428,7 +428,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.302", + "version": "0.8.400", "dependencies": { "axios": "^1.13.5", "dayjs": "^1.11.13", @@ -465,7 +465,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.38", + "version": "0.0.39", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^29.0.0", @@ -917,6 +917,8 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], @@ -1485,7 +1487,7 @@ "@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="], - "@librechat/agents": ["@librechat/agents@3.1.55", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.980.0", "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.80", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "@scarf/scarf": "^1.4.0", "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "okapibm25": "^1.4.1", "openai": "5.8.2" } }, "sha512-impxeKpCDlPkAVQFWnA6u6xkxDSBR/+H8uYq7rZomBeu0rUh/OhJLiI1fAwPhKXP33udNtHA8GyDi0QJj78R9w=="], + "@librechat/agents": ["@librechat/agents@3.1.56", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.980.0", "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.80", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "@scarf/scarf": "^1.4.0", "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "okapibm25": "^1.4.1", "openai": "5.8.2" } }, "sha512-HJJwRnLM4XKpTWB4/wPDJR+iegyKBVUwqj7A8QHqzEcHzjKJDTr3wBPxZVH1tagGr6/mbbnErOJ14cH1OSNmpA=="], "@librechat/api": ["@librechat/api@workspace:packages/api"], @@ -1589,11 +1591,11 @@ "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="], - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], @@ -1813,25 +1815,25 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^2.3.1" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.37.0", "", { "os": "android", "cpu": "arm" }, "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.37.0", "", { "os": "android", "cpu": "arm64" }, "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.37.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.37.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.37.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.37.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], @@ -1845,27 +1847,27 @@ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.37.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.37.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.37.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -2009,6 +2011,8 @@ "@testing-library/user-event": ["@testing-library/user-event@14.5.2", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], @@ -2151,7 +2155,7 @@ "@types/multer": ["@types/multer@1.4.13", "", { "dependencies": { "@types/express": "*" } }, "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw=="], - "@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -2295,6 +2299,8 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ai-tokenizer": ["ai-tokenizer@1.0.6", "", { "peerDependencies": { "ai": "^5.0.0" }, "optionalPeers": ["ai"] }, "sha512-GaakQFxen0pRH/HIA4v68ZM40llCH27HUYUSBLK+gVuZ57e53pYJe1xFvSTj4sJJjbWU92m1X6NjPWyeWkFDow=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -2755,7 +2761,7 @@ "date-fns": ["date-fns@3.3.1", "", {}, "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw=="], - "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], + "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -3037,7 +3043,7 @@ "file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="], - "file-type": ["file-type@18.7.0", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.2", "strtok3": "^7.0.0", "token-types": "^5.0.1" } }, "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw=="], + "file-type": ["file-type@21.3.3", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-pNwbwz8c3aZ+GvbJnIsCnDjKvgCZLHxkFWLEFxU3RMa+Ey++ZSEfisvsWQMcdys6PpxQjWUOIDi1fifXsW3YRg=="], "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], @@ -3839,7 +3845,7 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.8", "", { "bin": "bin/nanoid.cjs" }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": "lib/cli.js" }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], @@ -4021,8 +4027,6 @@ "pdfjs-dist": ["pdfjs-dist@5.5.207", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.95", "node-readable-to-web-readable-stream": "^0.4.2" } }, "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw=="], - "peek-readable": ["peek-readable@5.0.0", "", {}, "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A=="], - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -4297,8 +4301,6 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.2", "", { "dependencies": { "readable-stream": "^3.6.0" } }, "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "recoil": ["recoil@0.7.7", "", { "dependencies": { "hamt_plus": "1.0.2" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ=="], @@ -4379,7 +4381,7 @@ "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - "rollup": ["rollup@4.37.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "rollup-plugin-peer-deps-external": ["rollup-plugin-peer-deps-external@2.2.4", "", { "peerDependencies": { "rollup": "*" } }, "sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g=="], @@ -4563,7 +4565,7 @@ "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "strtok3": ["strtok3@7.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.0.0" } }, "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ=="], + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], "style-inject": ["style-inject@0.3.0", "", {}, "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw=="], @@ -4627,8 +4629,6 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tiktoken": ["tiktoken@1.0.15", "", {}, "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw=="], - "timers-browserify": ["timers-browserify@2.0.12", "", { "dependencies": { "setimmediate": "^1.0.4" } }, "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ=="], "tiny-emitter": ["tiny-emitter@2.1.0", "", {}, "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="], @@ -4651,7 +4651,7 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "token-types": ["token-types@5.0.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg=="], + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], "touch": ["touch@3.1.0", "", { "dependencies": { "nopt": "~1.0.10" }, "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA=="], @@ -4735,15 +4735,17 @@ "uid2": ["uid2@0.0.4", "", {}, "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], - "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + "undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="], - "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], @@ -5529,6 +5531,8 @@ "@google/genai/google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], + "@grpc/grpc-js/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@grpc/proto-loader/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "@headlessui/react/@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], @@ -5549,16 +5553,30 @@ "@istanbuljs/load-nyc-config/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "@jest/console/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@jest/console/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "@jest/core/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@jest/core/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "@jest/core/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "@jest/environment/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@jest/environment-jsdom-abstract/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@jest/expect/expect": ["expect@30.2.0", "", { "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw=="], + "@jest/fake-timers/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@jest/pattern/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@jest/reporters/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@jest/reporters/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "@jest/reporters/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -5571,6 +5589,8 @@ "@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "@jest/types/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -5597,16 +5617,12 @@ "@langchain/mistralai/uuid": ["uuid@10.0.0", "", { "bin": "dist/bin/uuid" }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "@librechat/agents/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "@librechat/backend/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.6", "", { "dependencies": { "@smithy/abort-controller": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ=="], + "@librechat/client/rollup": ["rollup@4.37.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="], "@librechat/frontend/@react-spring/web": ["@react-spring/web@9.7.5", "", { "dependencies": { "@react-spring/animated": "~9.7.5", "@react-spring/core": "~9.7.5", "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ=="], "@librechat/frontend/@testing-library/jest-dom": ["@testing-library/jest-dom@5.17.0", "", { "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.5.6", "lodash": "^4.17.15", "redent": "^3.0.0" } }, "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg=="], - "@librechat/frontend/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "@librechat/frontend/dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="], "@librechat/frontend/framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], @@ -5629,45 +5645,9 @@ "@node-saml/passport-saml/passport": ["passport@0.7.0", "", { "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", "utils-merge": "^1.0.1" } }, "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ=="], - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], "@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], @@ -5931,13 +5911,33 @@ "@testing-library/dom/pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "@types/body-parser/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/connect/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/express-serve-static-core/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/jsdom/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/jsonwebtoken/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/ldapjs/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@types/mdast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], + "@types/node-fetch/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/send/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/serve-static/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@types/testing-library__jest-dom/@types/jest": ["@types/jest@29.5.12", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw=="], "@types/winston/winston": ["winston@3.11.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.4.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.5.0" } }, "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g=="], - "@types/ws/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + "@types/xml-encryption/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@types/xml2js/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], "@typescript-eslint/parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], @@ -5981,6 +5981,8 @@ "browserify-sign/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "bun-types/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "caniuse-api/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -6087,8 +6089,6 @@ "google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], - "happy-dom/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "happy-dom/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], @@ -6151,6 +6151,8 @@ "jest-changed-files/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + "jest-circus/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-circus/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], "jest-circus/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], @@ -6167,8 +6169,14 @@ "jest-each/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], + "jest-environment-jsdom/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "jest-environment-node/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-file-loader/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "jest-haste-map/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-leak-detector/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], @@ -6177,10 +6185,16 @@ "jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "jest-mock/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-resolve/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "jest-runner/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + "jest-runtime/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-runtime/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "jest-runtime/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -6199,8 +6213,14 @@ "jest-snapshot/synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], + "jest-util/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-validate/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], + "jest-watcher/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "jest-worker/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -6233,8 +6253,6 @@ "ldapauth-fork/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "librechat-data-provider/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "lint-staged/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], @@ -6259,8 +6277,6 @@ "memorystore/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "mermaid/dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], - "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], "mermaid/uuid": ["uuid@11.1.0", "", { "bin": "dist/esm/bin/uuid" }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -6317,6 +6333,8 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "postcss/nanoid": ["nanoid@3.3.8", "", { "bin": "bin/nanoid.cjs" }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + "postcss-attribute-case-insensitive/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "postcss-colormin/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], @@ -6365,8 +6383,6 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "protobufjs/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "public-encrypt/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "rc-util/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -6399,6 +6415,8 @@ "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "rollup-plugin-postcss/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "rollup-plugin-typescript2/@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], @@ -6495,8 +6513,6 @@ "vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], - "vite/rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], - "winston-daily-rotate-file/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], "workbox-build/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], @@ -6863,6 +6879,10 @@ "@google/genai/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], + "@grpc/grpc-js/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@grpc/proto-loader/protobufjs/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@headlessui/react/@tanstack/react-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -6871,18 +6891,34 @@ "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "@jest/console/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/core/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@jest/environment-jsdom-abstract/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/environment/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/expect/expect/@jest/expect-utils": ["@jest/expect-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0" } }, "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA=="], "@jest/expect/expect/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], + "@jest/fake-timers/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/pattern/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/reporters/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/reporters/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@jest/reporters/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "@jest/reporters/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.927.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@aws-sdk/xml-builder": "3.921.0", "@smithy/core": "^3.17.2", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/signature-v4": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.922.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-DTKHeH1Bk17zSdoa5qXPGwCmZXuhQReqXOVW2/jIVX8NGVvnraH7WppGPlQxBjFtwSSwVTgzH2NVPgediQphNA=="], @@ -6999,13 +7035,41 @@ "@langchain/google-gauth/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], - "@librechat/backend/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg=="], + "@librechat/client/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.37.0", "", { "os": "android", "cpu": "arm" }, "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ=="], - "@librechat/backend/@smithy/node-http-handler/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], + "@librechat/client/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.37.0", "", { "os": "android", "cpu": "arm64" }, "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA=="], - "@librechat/backend/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg=="], + "@librechat/client/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.37.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA=="], - "@librechat/backend/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@librechat/client/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.37.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ=="], + + "@librechat/client/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.37.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA=="], + + "@librechat/client/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.37.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA=="], + + "@librechat/client/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w=="], + + "@librechat/client/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag=="], + + "@librechat/client/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA=="], + + "@librechat/client/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ=="], + + "@librechat/client/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw=="], + + "@librechat/client/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA=="], + + "@librechat/client/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.37.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A=="], + + "@librechat/client/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ=="], + + "@librechat/client/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w=="], + + "@librechat/client/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.37.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg=="], + + "@librechat/client/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.37.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA=="], + + "@librechat/client/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="], "@librechat/frontend/@react-spring/web/@react-spring/animated": ["@react-spring/animated@9.7.5", "", { "dependencies": { "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg=="], @@ -7025,8 +7089,6 @@ "@librechat/frontend/@testing-library/jest-dom/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "@librechat/frontend/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@librechat/frontend/framer-motion/motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], "@librechat/frontend/framer-motion/motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="], @@ -7045,27 +7107,13 @@ "@node-saml/passport-saml/@types/express/@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - - "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + "@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], @@ -7171,11 +7219,31 @@ "@smithy/credential-provider-imds/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@3.0.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ=="], + "@types/body-parser/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/connect/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/jsdom/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/jsonwebtoken/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/ldapjs/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/node-fetch/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/send/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/serve-static/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@types/winston/winston/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], "@types/winston/winston/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], - "@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/xml-encryption/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/xml2js/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -7205,6 +7273,8 @@ "browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "caniuse-api/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "caniuse-api/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], @@ -7243,6 +7313,8 @@ "expect/jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + "expect/jest-util/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "expect/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "expect/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -7265,8 +7337,6 @@ "google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], - "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "hast-util-from-html-isomorphic/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], "hast-util-from-html/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], @@ -7303,6 +7373,8 @@ "jest-changed-files/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + "jest-circus/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -7317,10 +7389,22 @@ "jest-each/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-environment-jsdom/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-environment-node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-haste-map/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-leak-detector/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-mock/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-runner/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-runtime/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-runtime/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "jest-runtime/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -7331,8 +7415,14 @@ "jest-snapshot/synckit/@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + "jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-watcher/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-worker/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "jsdom/whatwg-url/tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], @@ -7345,8 +7435,6 @@ "jwks-rsa/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], - "librechat-data-provider/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], @@ -7401,8 +7489,6 @@ "pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "protobufjs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "rehype-highlight/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], "rehype-highlight/unified/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], @@ -7457,46 +7543,6 @@ "vfile-location/vfile/vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="], - "vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - - "vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - - "vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - - "vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - - "vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - - "vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - - "vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - - "vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - - "vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - - "vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - - "vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - - "vite/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - - "vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - - "vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - - "vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - - "vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - - "vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - - "vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], - - "vite/rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "winston-daily-rotate-file/winston-transport/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], "workbox-build/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], @@ -7813,6 +7859,8 @@ "@google/genai/google-auth-library/gaxios/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": "dist/esm/bin.mjs" }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "@grpc/proto-loader/protobufjs/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@jest/expect/expect/jest-matcher-utils/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], @@ -7923,8 +7971,6 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": "dist/esm/bin.mjs" }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], - "@librechat/backend/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/frontend/@react-spring/web/@react-spring/shared/@react-spring/rafz": ["@react-spring/rafz@9.7.5", "", {}, "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw=="], "@librechat/frontend/@testing-library/jest-dom/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -7937,9 +7983,13 @@ "@mcp-ui/client/@modelcontextprotocol/sdk/express/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@node-saml/passport-saml/@types/express/@types/express-serve-static-core/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "@node-saml/passport-saml/@types/express/@types/express-serve-static-core/@types/qs": ["@types/qs@6.9.17", "", {}, "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ=="], - "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + + "@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], @@ -7971,8 +8021,12 @@ "expect/jest-message-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "expect/jest-message-util/@jest/types/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "expect/jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "expect/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "express-static-gzip/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "express-static-gzip/serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -7999,6 +8053,8 @@ "jsdom/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "jwks-rsa/@types/express/@types/express-serve-static-core/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], + "mongodb-connection-string-url/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "multer/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -8485,6 +8541,10 @@ "@librechat/frontend/@testing-library/jest-dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "@node-saml/passport-saml/@types/express/@types/express-serve-static-core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], @@ -8495,10 +8555,14 @@ "expect/jest-message-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "expect/jest-message-util/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "expect/jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], "express-static-gzip/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "jwks-rsa/@types/express/@types/express-serve-static-core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "svgo/css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 1c698d08a3..f97adb39ce 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -1,4 +1,4 @@ -/** v0.8.3 */ +/** v0.8.4-rc1 */ module.exports = { roots: ['/src'], testEnvironment: 'jsdom', diff --git a/client/package.json b/client/package.json index 250afc9990..a3ff5529e5 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/frontend", - "version": "v0.8.3", + "version": "v0.8.4-rc1", "description": "", "type": "module", "scripts": { diff --git a/e2e/jestSetup.js b/e2e/jestSetup.js index 64c1a8546f..f6d5bf4c66 100644 --- a/e2e/jestSetup.js +++ b/e2e/jestSetup.js @@ -1,3 +1,3 @@ -// v0.8.3 +// v0.8.4-rc1 // See .env.test.example for an example of the '.env.test' file. require('dotenv').config({ path: './e2e/.env.test' }); diff --git a/helm/librechat/Chart.yaml b/helm/librechat/Chart.yaml index a2dff261c7..d39ec8811c 100755 --- a/helm/librechat/Chart.yaml +++ b/helm/librechat/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.0.0 +version: 2.0.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,7 +23,7 @@ version: 2.0.0 # It is recommended to use it with quotes. # renovate: image=registry.librechat.ai/danny-avila/librechat -appVersion: "v0.8.3" +appVersion: "v0.8.4-rc1" home: https://www.librechat.ai diff --git a/package-lock.json b/package-lock.json index 45f737ad8f..f39ae2d180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "LibreChat", - "version": "v0.8.3", + "version": "v0.8.4-rc1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "LibreChat", - "version": "v0.8.3", + "version": "v0.8.4-rc1", "license": "ISC", "workspaces": [ "api", @@ -46,7 +46,7 @@ }, "api": { "name": "@librechat/backend", - "version": "v0.8.3", + "version": "v0.8.4-rc1", "license": "ISC", "dependencies": { "@anthropic-ai/vertex-sdk": "^0.14.3", @@ -429,7 +429,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "v0.8.3", + "version": "v0.8.4-rc1", "license": "ISC", "dependencies": { "@ariakit/react": "^0.4.15", @@ -44195,7 +44195,7 @@ }, "packages/api": { "name": "@librechat/api", - "version": "1.7.25", + "version": "1.7.26", "license": "ISC", "devDependencies": { "@babel/preset-env": "^7.21.5", @@ -44311,7 +44311,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.4.54", + "version": "0.4.55", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -46135,7 +46135,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.302", + "version": "0.8.400", "license": "ISC", "dependencies": { "axios": "^1.13.5", @@ -46193,7 +46193,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.38", + "version": "0.0.39", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", diff --git a/package.json b/package.json index ecbede482e..6605752f39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LibreChat", - "version": "v0.8.3", + "version": "v0.8.4-rc1", "description": "", "packageManager": "npm@11.10.0", "workspaces": [ diff --git a/packages/api/package.json b/packages/api/package.json index b3b40c79a2..46fbeb02b6 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/api", - "version": "1.7.25", + "version": "1.7.26", "type": "commonjs", "description": "MCP services for LibreChat", "main": "dist/index.js", diff --git a/packages/client/package.json b/packages/client/package.json index 13d1a4a8cc..e76c1d075a 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.4.54", + "version": "0.4.55", "description": "React components for LibreChat", "repository": { "type": "git", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index a707aef448..09e13b31a7 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.8.302", + "version": "0.8.400", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index bb0c180209..e19c69e799 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1740,7 +1740,7 @@ export enum TTSProviders { /** Enum for app-wide constants */ export enum Constants { /** Key for the app's version. */ - VERSION = 'v0.8.3', + VERSION = 'v0.8.4-rc1', /** Key for the Custom Config's version (librechat.yaml). */ CONFIG_VERSION = '1.3.6', /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */ diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index e91bfb8886..87acd16f1e 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/data-schemas", - "version": "0.0.38", + "version": "0.0.39", "description": "Mongoose schemas and models for LibreChat", "type": "module", "main": "dist/index.cjs", From b5a55b23a4b2f5f6428679b1ee358f18b02d51d8 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 17 Mar 2026 17:04:18 -0400 Subject: [PATCH 064/111] =?UTF-8?q?=F0=9F=93=A6=20chore:=20NPM=20audit=20p?= =?UTF-8?q?ackages=20(#12286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 chore: Update dependencies in package-lock.json and package.json - Bump @aws-sdk/client-bedrock-runtime from 3.980.0 to 3.1011.0 and update related dependencies. - Update fast-xml-parser version from 5.3.8 to 5.5.6 in package.json. - Adjust various @aws-sdk and @smithy packages to their latest versions for improved functionality and security. * 🔧 chore: Update @librechat/agents dependency to version 3.1.57 in package.json and package-lock.json - Bump @librechat/agents from 3.1.56 to 3.1.57 across multiple package files for consistency. - Remove axios dependency from package.json as it is no longer needed. --- api/package.json | 2 +- package-lock.json | 1998 +++++++++++++++---------------------- package.json | 7 +- packages/api/package.json | 2 +- 4 files changed, 800 insertions(+), 1209 deletions(-) diff --git a/api/package.json b/api/package.json index f32de5e778..2255679dae 100644 --- a/api/package.json +++ b/api/package.json @@ -44,7 +44,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.56", + "@librechat/agents": "^3.1.57", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index f39ae2d180..2454745a79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.56", + "@librechat/agents": "^3.1.57", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -3199,57 +3199,73 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.980.0.tgz", - "integrity": "sha512-agRy8K543Q4WxCiup12JiSe4rO2gkw4wykaGXD+MEmzG2Nq4ODvKrNHT+XYCyTvk9ehJim/vpu+Stae3nEI0yw==", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1011.0.tgz", + "integrity": "sha512-yn5oRLLP1TsGLZqlnyqBjAVmiexYR8/rPG8D+rI5f5+UIvb3zHOmHLXA1m41H/sKXI4embmXfUjvArmjTmfsIw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/credential-provider-node": "^3.972.4", - "@aws-sdk/eventstream-handler-node": "^3.972.3", - "@aws-sdk/middleware-eventstream": "^3.972.3", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.5", - "@aws-sdk/middleware-websocket": "^3.972.3", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/token-providers": "3.980.0", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.980.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.3", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/eventstream-serde-browser": "^4.2.8", - "@smithy/eventstream-serde-config-resolver": "^4.3.8", - "@smithy/eventstream-serde-node": "^4.2.8", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/eventstream-handler-node": "^3.972.11", + "@aws-sdk/middleware-eventstream": "^3.972.8", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/middleware-websocket": "^3.972.13", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/token-providers": "3.1011.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -3257,9 +3273,9 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3269,12 +3285,12 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3282,12 +3298,12 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3465,24 +3481,24 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.5.tgz", - "integrity": "sha512-3IgeIDiQ15tmMBFIdJ1cTy3A9rXHGo+b9p22V38vA3MozeMyVC8VmCYdDLA0iMWo4VHA9LDJTgCM0+xU3wjBOg==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.20.tgz", + "integrity": "sha512-yhva/xL5H4tWQgsBjwV+RRD0ByCzg0TcByDCLp3GXdn/wlyRNfy8zsswDtCvr1WSKQkSQYlyEzPuWkJG0f5HvQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-arn-parser": "^3.972.2", - "@smithy/core": "^3.22.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3507,9 +3523,9 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", - "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3519,9 +3535,9 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3531,12 +3547,12 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3544,115 +3560,12 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.982.0.tgz", - "integrity": "sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3660,23 +3573,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.6.tgz", - "integrity": "sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw==", + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/xml-builder": "^3.972.4", - "@smithy/core": "^3.22.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3684,9 +3597,9 @@ } }, "node_modules/@aws-sdk/core/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3696,12 +3609,12 @@ } }, "node_modules/@aws-sdk/core/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3709,12 +3622,12 @@ } }, "node_modules/@aws-sdk/core/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3722,12 +3635,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", - "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", + "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3735,15 +3648,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.4.tgz", - "integrity": "sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3751,20 +3664,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.6.tgz", - "integrity": "sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", "tslib": "^2.6.2" }, "engines": { @@ -3772,272 +3685,66 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.4.tgz", - "integrity": "sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/credential-provider-env": "^3.972.4", - "@aws-sdk/credential-provider-http": "^3.972.6", - "@aws-sdk/credential-provider-login": "^3.972.4", - "@aws-sdk/credential-provider-process": "^3.972.4", - "@aws-sdk/credential-provider-sso": "^3.972.4", - "@aws-sdk/credential-provider-web-identity": "^3.972.4", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/nested-clients": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", - "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.4.tgz", - "integrity": "sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", - "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.5.tgz", - "integrity": "sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.4", - "@aws-sdk/credential-provider-http": "^3.972.6", - "@aws-sdk/credential-provider-ini": "^3.972.4", - "@aws-sdk/credential-provider-process": "^3.972.4", - "@aws-sdk/credential-provider-sso": "^3.972.4", - "@aws-sdk/credential-provider-web-identity": "^3.972.4", - "@aws-sdk/types": "^3.973.1", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4045,16 +3752,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.4.tgz", - "integrity": "sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4062,67 +3769,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.4.tgz", - "integrity": "sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.982.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/token-providers": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/nested-clients": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", - "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4130,207 +3788,50 @@ } }, "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.982.0.tgz", - "integrity": "sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw==", + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.4.tgz", - "integrity": "sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/nested-clients": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", - "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.3.tgz", - "integrity": "sha512-uQbkXcfEj4+TrxTmZkSwsYRE9nujx9b6WeLoQkDsldzEpcQhtKIz/RHSB4lWe7xzDMfGCLUkwmSJjetGVcrhCw==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.11.tgz", + "integrity": "sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/eventstream-codec": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4368,14 +3869,14 @@ } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.3.tgz", - "integrity": "sha512-pbvZ6Ye/Ks6BAZPa3RhsNjHrvxU9li25PMhSdDpbX0jzdpKpAkIR65gXSNKmA/REnSdEMWSD4vKUW+5eMFzB6w==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.8.tgz", + "integrity": "sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4398,24 +3899,24 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.3.tgz", - "integrity": "sha512-MkNGJ6qB9kpsLwL18kC/ZXppsJbftHVGCisqpEVbTQsum8CLYDX1Bmp/IvhRGNxsqCO2w9/4PwhDKBjG3Uvr4Q==", + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.0.tgz", + "integrity": "sha512-BmdDjqvnuYaC4SY7ypHLXfCSsGYGUZkjCLSZyUAAYn1YT28vbNMJNDwhlfkvvE+hQHG5RJDlEmYuvBxcB9jX1g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/crc64-nvme": "3.972.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/crc64-nvme": "^3.972.5", + "@aws-sdk/types": "^3.973.6", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4423,9 +3924,9 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4435,12 +3936,12 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4448,12 +3949,12 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4461,14 +3962,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", - "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4490,13 +3991,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", - "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4504,15 +4005,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", - "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", + "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4632,17 +4133,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.6.tgz", - "integrity": "sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@smithy/core": "^3.22.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -4650,15 +4152,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -4666,20 +4168,22 @@ } }, "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.3.tgz", - "integrity": "sha512-/BjMbtOM9lsgdNgRZWUL5oCV6Ocfx1vcK/C5xO5/t/gCk6IwR9JFWMilbk6K6Buq5F84/lkngqcCKU2SRkAmOg==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.13.tgz", + "integrity": "sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-format-url": "^3.972.3", - "@smithy/eventstream-codec": "^4.2.8", - "@smithy/eventstream-serde-browser": "^4.2.8", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-format-url": "^3.972.8", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4687,63 +4191,117 @@ } }, "node_modules/@aws-sdk/middleware-websocket/node_modules/@aws-sdk/util-format-url": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.3.tgz", - "integrity": "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", + "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-websocket/node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket/node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket/node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz", - "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==", + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.5", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.980.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.3", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -4751,9 +4309,9 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4763,12 +4321,12 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4776,12 +4334,12 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4789,15 +4347,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", - "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/config-resolver": "^4.4.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4867,17 +4425,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz", - "integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1011.0.tgz", + "integrity": "sha512-WSfBVDQ9uyh1GCR+DxxgHEvAKv+beMIlSeJ2pMAG1HTci340+xbtz1VFwnTJ5qCxrMi+E4dyDMiSAhDvHnq73A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/nested-clients": "3.980.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4885,12 +4443,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", - "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4965,27 +4523,28 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", - "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.4.tgz", - "integrity": "sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A==", + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5001,13 +4560,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", - "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.3.8", + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" }, "engines": { @@ -12324,9 +11883,9 @@ } }, "node_modules/@librechat/agents": { - "version": "3.1.56", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.56.tgz", - "integrity": "sha512-HJJwRnLM4XKpTWB4/wPDJR+iegyKBVUwqj7A8QHqzEcHzjKJDTr3wBPxZVH1tagGr6/mbbnErOJ14cH1OSNmpA==", + "version": "3.1.57", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.57.tgz", + "integrity": "sha512-fP/ZF7a7QL/MhXTfdzpG3cpOai9LSiKMiFX1X23o3t67Bqj9r5FuSVgu+UHDfO7o4Np82ZWw2nQJjcMJQbArLA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -19584,12 +19143,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", - "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -19622,16 +19181,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", - "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -19639,20 +19198,20 @@ } }, "node_modules/@smithy/core": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.0.tgz", - "integrity": "sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA==", + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.9", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -19660,9 +19219,9 @@ } }, "node_modules/@smithy/core/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19672,12 +19231,12 @@ } }, "node_modules/@smithy/core/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -19685,12 +19244,12 @@ } }, "node_modules/@smithy/core/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -19698,15 +19257,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", - "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -19714,14 +19273,14 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", - "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", + "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -19729,13 +19288,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", - "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", + "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -19743,12 +19302,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", - "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", + "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -19756,13 +19315,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", - "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", + "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -19770,13 +19329,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", - "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", + "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -19784,15 +19343,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", - "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -19815,14 +19374,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", - "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -19830,9 +19389,9 @@ } }, "node_modules/@smithy/hash-node/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19842,12 +19401,12 @@ } }, "node_modules/@smithy/hash-node/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -19855,12 +19414,12 @@ } }, "node_modules/@smithy/hash-node/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -19920,12 +19479,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", - "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -19996,13 +19555,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", - "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20010,18 +19569,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.12.tgz", - "integrity": "sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q==", + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.0", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -20029,19 +19588,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.29", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.29.tgz", - "integrity": "sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg==", + "version": "4.4.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/uuid": "^1.1.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -20049,13 +19608,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", - "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20063,12 +19623,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", - "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20076,14 +19636,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", - "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20091,15 +19651,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", - "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20107,12 +19667,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", - "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20120,12 +19680,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", - "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20133,13 +19693,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", - "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-uri-escape": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20147,12 +19707,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", - "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20160,24 +19720,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", - "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0" + "@smithy/types": "^4.13.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", - "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20185,18 +19745,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", - "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20204,9 +19764,9 @@ } }, "node_modules/@smithy/signature-v4/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20216,12 +19776,12 @@ } }, "node_modules/@smithy/signature-v4/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20229,12 +19789,12 @@ } }, "node_modules/@smithy/signature-v4/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20242,17 +19802,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.1.tgz", - "integrity": "sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ==", + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.0", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -20260,9 +19820,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20272,13 +19832,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", - "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20286,13 +19846,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20300,9 +19860,9 @@ } }, "node_modules/@smithy/util-base64/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20312,12 +19872,12 @@ } }, "node_modules/@smithy/util-base64/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20325,12 +19885,12 @@ } }, "node_modules/@smithy/util-base64/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20338,9 +19898,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20350,9 +19910,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20374,9 +19934,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20386,14 +19946,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.28", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.28.tgz", - "integrity": "sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w==", + "version": "4.3.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20401,17 +19961,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.31", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.31.tgz", - "integrity": "sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA==", + "version": "4.2.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.6", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20419,13 +19979,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", - "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20433,9 +19993,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20445,12 +20005,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", - "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20458,13 +20018,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", - "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -20472,18 +20032,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.10", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", - "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20491,9 +20051,9 @@ } }, "node_modules/@smithy/util-stream/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20503,12 +20063,12 @@ } }, "node_modules/@smithy/util-stream/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20516,12 +20076,12 @@ } }, "node_modules/@smithy/util-stream/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -20529,9 +20089,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -20567,9 +20127,9 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -27504,10 +27064,10 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-parser": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.8.tgz", - "integrity": "sha512-53jIF4N6u/pxvaL1eb/hEZts/cFLWZ92eCfLrNyCI0k38lettCG/Bs40W9pPwoPXyHQlKu2OUbQtiEIZK/J6Vw==", + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "funding": [ { "type": "github", @@ -27516,7 +27076,24 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", + "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -27813,9 +27390,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -35689,6 +35266,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -44240,7 +43832,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.56", + "@librechat/agents": "^3.1.57", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", diff --git a/package.json b/package.json index 6605752f39..e59032c7dd 100644 --- a/package.json +++ b/package.json @@ -139,14 +139,13 @@ "@librechat/agents": { "@langchain/anthropic": { "@anthropic-ai/sdk": "0.73.0", - "fast-xml-parser": "5.3.8" + "fast-xml-parser": "5.5.6" }, "@anthropic-ai/sdk": "0.73.0", - "fast-xml-parser": "5.3.8" + "fast-xml-parser": "5.5.6" }, - "axios": "1.12.1", "elliptic": "^6.6.1", - "fast-xml-parser": "5.3.8", + "fast-xml-parser": "5.5.6", "form-data": "^4.0.4", "tslib": "^2.8.1", "mdast-util-gfm-autolink-literal": "2.0.0", diff --git a/packages/api/package.json b/packages/api/package.json index 46fbeb02b6..42f1f0e9f0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -90,7 +90,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.56", + "@librechat/agents": "^3.1.57", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", From 9cb5ac63f89210dc506f7cdd64f127265629d781 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 14:51:28 -0400 Subject: [PATCH 065/111] =?UTF-8?q?=F0=9F=AB=A7=20refactor:=20Clear=20Draf?= =?UTF-8?q?ts=20and=20Surface=20Error=20on=20Expired=20SSE=20Stream=20(#12?= =?UTF-8?q?309)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: error handling in useResumableSSE for 404 responses - Added logic to clear drafts from localStorage when a 404 error occurs. - Integrated errorHandler to notify users of the error condition. - Introduced comprehensive tests to validate the new behavior, ensuring drafts are cleared and error handling is triggered correctly.C * feat: add STREAM_EXPIRED error handling and message localization - Introduced handling for STREAM_EXPIRED errors in useResumableSSE, updating errorHandler to provide relevant feedback. - Added a new error message for STREAM_EXPIRED in translation files for user notifications. - Updated tests to ensure proper error handling and message verification for STREAM_EXPIRED scenarios. * refactor: replace clearDraft with clearAllDrafts utility - Removed the clearDraft function from useResumableSSE and useSSE hooks, replacing it with the new clearAllDrafts utility for better draft management. - Updated localStorage interactions to ensure both text and file drafts are cleared consistently for a conversation. - Enhanced code readability and maintainability by centralizing draft clearing logic. --- .../src/components/Messages/Content/Error.tsx | 1 + .../SSE/__tests__/useResumableSSE.spec.ts | 273 ++++++++++++++++++ client/src/hooks/SSE/useResumableSSE.ts | 22 +- client/src/hooks/SSE/useSSE.ts | 22 +- client/src/locales/en/translation.json | 1 + client/src/utils/drafts.ts | 9 +- packages/data-provider/src/config.ts | 4 + 7 files changed, 299 insertions(+), 33 deletions(-) create mode 100644 client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts diff --git a/client/src/components/Messages/Content/Error.tsx b/client/src/components/Messages/Content/Error.tsx index ff2f2d7e90..b464ce2f2a 100644 --- a/client/src/components/Messages/Content/Error.tsx +++ b/client/src/components/Messages/Content/Error.tsx @@ -75,6 +75,7 @@ const errorMessages = { return info; }, [ErrorTypes.GOOGLE_TOOL_CONFLICT]: 'com_error_google_tool_conflict', + [ErrorTypes.STREAM_EXPIRED]: 'com_error_stream_expired', [ViolationTypes.BAN]: 'Your account has been temporarily banned due to violations of our service.', [ViolationTypes.ILLEGAL_MODEL_REQUEST]: (json: TGenericError, localize: LocalizeFunction) => { diff --git a/client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts b/client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts new file mode 100644 index 0000000000..9100f39858 --- /dev/null +++ b/client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts @@ -0,0 +1,273 @@ +import { renderHook, act } from '@testing-library/react'; +import { Constants, ErrorTypes, LocalStorageKeys } from 'librechat-data-provider'; +import type { TSubmission } from 'librechat-data-provider'; + +type SSEEventListener = (e: Partial & { responseCode?: number }) => void; + +interface MockSSEInstance { + addEventListener: jest.Mock; + stream: jest.Mock; + close: jest.Mock; + headers: Record; + _listeners: Record; + _emit: (event: string, data?: Partial & { responseCode?: number }) => void; +} + +const mockSSEInstances: MockSSEInstance[] = []; + +jest.mock('sse.js', () => ({ + SSE: jest.fn().mockImplementation(() => { + const listeners: Record = {}; + const instance: MockSSEInstance = { + addEventListener: jest.fn((event: string, cb: SSEEventListener) => { + listeners[event] = cb; + }), + stream: jest.fn(), + close: jest.fn(), + headers: {}, + _listeners: listeners, + _emit: (event, data = {}) => listeners[event]?.(data as MessageEvent), + }; + mockSSEInstances.push(instance); + return instance; + }), +})); + +const mockSetQueryData = jest.fn(); +const mockQueryClient = { setQueryData: mockSetQueryData }; + +jest.mock('@tanstack/react-query', () => ({ + ...jest.requireActual('@tanstack/react-query'), + useQueryClient: () => mockQueryClient, +})); + +jest.mock('recoil', () => ({ + ...jest.requireActual('recoil'), + useSetRecoilState: () => jest.fn(), +})); + +jest.mock('~/store', () => ({ + __esModule: true, + default: { + activeRunFamily: jest.fn(), + abortScrollFamily: jest.fn(), + showStopButtonByIndex: jest.fn(), + }, +})); + +jest.mock('~/hooks/AuthContext', () => ({ + useAuthContext: () => ({ token: 'test-token', isAuthenticated: true }), +})); + +jest.mock('~/data-provider', () => ({ + useGetStartupConfig: () => ({ data: { balance: { enabled: false } } }), + useGetUserBalance: () => ({ refetch: jest.fn() }), + queueTitleGeneration: jest.fn(), +})); + +const mockErrorHandler = jest.fn(); +const mockSetIsSubmitting = jest.fn(); +const mockClearStepMaps = jest.fn(); + +jest.mock('~/hooks/SSE/useEventHandlers', () => + jest.fn(() => ({ + errorHandler: mockErrorHandler, + finalHandler: jest.fn(), + createdHandler: jest.fn(), + attachmentHandler: jest.fn(), + stepHandler: jest.fn(), + contentHandler: jest.fn(), + resetContentHandler: jest.fn(), + syncStepMessage: jest.fn(), + clearStepMaps: mockClearStepMaps, + messageHandler: jest.fn(), + setIsSubmitting: mockSetIsSubmitting, + setShowStopButton: jest.fn(), + })), +); + +jest.mock('librechat-data-provider', () => { + const actual = jest.requireActual('librechat-data-provider'); + return { + ...actual, + createPayload: jest.fn(() => ({ + payload: { model: 'gpt-4o' }, + server: '/api/agents/chat', + })), + removeNullishValues: jest.fn((v: unknown) => v), + apiBaseUrl: jest.fn(() => ''), + request: { + post: jest.fn().mockResolvedValue({ streamId: 'stream-123' }), + refreshToken: jest.fn(), + dispatchTokenUpdatedEvent: jest.fn(), + }, + }; +}); + +import useResumableSSE from '~/hooks/SSE/useResumableSSE'; + +const CONV_ID = 'conv-abc-123'; + +type PartialSubmission = { + conversation: { conversationId?: string }; + userMessage: Record; + messages: never[]; + isTemporary: boolean; + initialResponse: Record; + endpointOption: { endpoint: string }; +}; + +const buildSubmission = (overrides: Partial = {}): TSubmission => { + const conversationId = overrides.conversation?.conversationId ?? CONV_ID; + return { + conversation: { conversationId }, + userMessage: { + messageId: 'msg-1', + conversationId, + text: 'Hello', + isCreatedByUser: true, + sender: 'User', + parentMessageId: '00000000-0000-0000-0000-000000000000', + }, + messages: [], + isTemporary: false, + initialResponse: { + messageId: 'resp-1', + conversationId, + text: '', + isCreatedByUser: false, + sender: 'Assistant', + }, + endpointOption: { endpoint: 'agents' }, + ...overrides, + } as unknown as TSubmission; +}; + +const buildChatHelpers = () => ({ + setMessages: jest.fn(), + getMessages: jest.fn(() => []), + setConversation: jest.fn(), + setIsSubmitting: mockSetIsSubmitting, + newConversation: jest.fn(), + resetLatestMessage: jest.fn(), +}); + +const getLastSSE = (): MockSSEInstance => { + const sse = mockSSEInstances[mockSSEInstances.length - 1]; + expect(sse).toBeDefined(); + return sse; +}; + +describe('useResumableSSE - 404 error path', () => { + beforeEach(() => { + mockSSEInstances.length = 0; + localStorage.clear(); + }); + + const seedDraft = (conversationId: string) => { + localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`, 'draft text'); + localStorage.setItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`, '[]'); + }; + + const render404Scenario = async (conversationId = CONV_ID) => { + const submission = buildSubmission({ conversation: { conversationId } }); + const chatHelpers = buildChatHelpers(); + + const { unmount } = renderHook(() => useResumableSSE(submission, chatHelpers)); + + await act(async () => { + await Promise.resolve(); + }); + + const sse = getLastSSE(); + + await act(async () => { + sse._emit('error', { responseCode: 404 }); + }); + + return { sse, unmount, chatHelpers }; + }; + + it('clears the text and files draft from localStorage on 404', async () => { + seedDraft(CONV_ID); + expect(localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${CONV_ID}`)).not.toBeNull(); + expect(localStorage.getItem(`${LocalStorageKeys.FILES_DRAFT}${CONV_ID}`)).not.toBeNull(); + + const { unmount } = await render404Scenario(CONV_ID); + + expect(localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${CONV_ID}`)).toBeNull(); + expect(localStorage.getItem(`${LocalStorageKeys.FILES_DRAFT}${CONV_ID}`)).toBeNull(); + unmount(); + }); + + it('calls errorHandler with STREAM_EXPIRED error type on 404', async () => { + const { unmount } = await render404Scenario(CONV_ID); + + expect(mockErrorHandler).toHaveBeenCalledTimes(1); + const call = mockErrorHandler.mock.calls[0][0]; + expect(call.data).toBeDefined(); + const parsed = JSON.parse(call.data.text); + expect(parsed.type).toBe(ErrorTypes.STREAM_EXPIRED); + expect(call.submission).toEqual( + expect.objectContaining({ + conversation: expect.objectContaining({ conversationId: CONV_ID }), + }), + ); + unmount(); + }); + + it('clears both TEXT and FILES drafts for new-convo when conversationId is absent', async () => { + localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${Constants.NEW_CONVO}`, 'unsent message'); + localStorage.setItem(`${LocalStorageKeys.FILES_DRAFT}${Constants.NEW_CONVO}`, '[]'); + + const submission = buildSubmission({ conversation: {} }); + const chatHelpers = buildChatHelpers(); + + const { unmount } = renderHook(() => useResumableSSE(submission, chatHelpers)); + + await act(async () => { + await Promise.resolve(); + }); + + const sse = getLastSSE(); + await act(async () => { + sse._emit('error', { responseCode: 404 }); + }); + + expect(localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${Constants.NEW_CONVO}`)).toBeNull(); + expect( + localStorage.getItem(`${LocalStorageKeys.FILES_DRAFT}${Constants.NEW_CONVO}`), + ).toBeNull(); + unmount(); + }); + + it('closes the SSE connection on 404', async () => { + const { sse, unmount } = await render404Scenario(); + + expect(sse.close).toHaveBeenCalled(); + unmount(); + }); + + it.each([undefined, 500, 503])( + 'does not call errorHandler for responseCode %s (reconnect path)', + async (responseCode) => { + const submission = buildSubmission(); + const chatHelpers = buildChatHelpers(); + + const { unmount } = renderHook(() => useResumableSSE(submission, chatHelpers)); + + await act(async () => { + await Promise.resolve(); + }); + + const sse = getLastSSE(); + + await act(async () => { + sse._emit('error', { responseCode }); + }); + + expect(mockErrorHandler).not.toHaveBeenCalled(); + unmount(); + }, + ); +}); diff --git a/client/src/hooks/SSE/useResumableSSE.ts b/client/src/hooks/SSE/useResumableSSE.ts index 4d4cb4841a..ddfee30120 100644 --- a/client/src/hooks/SSE/useResumableSSE.ts +++ b/client/src/hooks/SSE/useResumableSSE.ts @@ -11,7 +11,6 @@ import { apiBaseUrl, createPayload, ViolationTypes, - LocalStorageKeys, removeNullishValues, } from 'librechat-data-provider'; import type { TMessage, TPayload, TSubmission, EventSubmission } from 'librechat-data-provider'; @@ -20,18 +19,9 @@ import { useGetStartupConfig, useGetUserBalance, queueTitleGeneration } from '~/ import type { ActiveJobsResponse } from '~/data-provider'; import { useAuthContext } from '~/hooks/AuthContext'; import useEventHandlers from './useEventHandlers'; +import { clearAllDrafts } from '~/utils'; import store from '~/store'; -const clearDraft = (conversationId?: string | null) => { - if (conversationId) { - localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`); - localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`); - } else { - localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${Constants.NEW_CONVO}`); - localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${Constants.NEW_CONVO}`); - } -}; - type ChatHelpers = Pick< EventHandlerParams, | 'setMessages' @@ -176,7 +166,7 @@ export default function useResumableSSE( conversationId: data.conversation?.conversationId, hasResponseMessage: !!data.responseMessage, }); - clearDraft(currentSubmission.conversation?.conversationId); + clearAllDrafts(currentSubmission.conversation?.conversationId); try { finalHandler(data, currentSubmission as EventSubmission); } catch (error) { @@ -357,7 +347,13 @@ export default function useResumableSSE( console.log('[ResumableSSE] Stream not found (404) - job completed or expired'); sse.close(); removeActiveJob(currentStreamId); - setIsSubmitting(false); + clearAllDrafts(currentSubmission.conversation?.conversationId); + errorHandler({ + data: { + text: JSON.stringify({ type: ErrorTypes.STREAM_EXPIRED }), + } as unknown as Parameters[0]['data'], + submission: currentSubmission as EventSubmission, + }); setShowStopButton(false); setStreamId(null); reconnectAttemptRef.current = 0; diff --git a/client/src/hooks/SSE/useSSE.ts b/client/src/hooks/SSE/useSSE.ts index ccdb252287..78835f5729 100644 --- a/client/src/hooks/SSE/useSSE.ts +++ b/client/src/hooks/SSE/useSSE.ts @@ -2,32 +2,16 @@ import { useEffect, useState } from 'react'; import { v4 } from 'uuid'; import { SSE } from 'sse.js'; import { useSetRecoilState } from 'recoil'; -import { - request, - Constants, - /* @ts-ignore */ - createPayload, - LocalStorageKeys, - removeNullishValues, -} from 'librechat-data-provider'; +import { request, createPayload, removeNullishValues } from 'librechat-data-provider'; import type { TMessage, TPayload, TSubmission, EventSubmission } from 'librechat-data-provider'; import type { EventHandlerParams } from './useEventHandlers'; import type { TResData } from '~/common'; import { useGetStartupConfig, useGetUserBalance } from '~/data-provider'; import { useAuthContext } from '~/hooks/AuthContext'; import useEventHandlers from './useEventHandlers'; +import { clearAllDrafts } from '~/utils'; import store from '~/store'; -const clearDraft = (conversationId?: string | null) => { - if (conversationId) { - localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`); - localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`); - } else { - localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${Constants.NEW_CONVO}`); - localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${Constants.NEW_CONVO}`); - } -}; - type ChatHelpers = Pick< EventHandlerParams, | 'setMessages' @@ -120,7 +104,7 @@ export default function useSSE( const data = JSON.parse(e.data); if (data.final != null) { - clearDraft(submission.conversation?.conversationId); + clearAllDrafts(submission.conversation?.conversationId); try { finalHandler(data, submission as EventSubmission); } catch (error) { diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index afd1072b61..9f641fdb16 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -376,6 +376,7 @@ "com_error_no_base_url": "No base URL found. Please provide one and try again.", "com_error_no_user_key": "No key found. Please provide a key and try again.", "com_error_refusal": "Response refused by safety filters. Rewrite your message and try again. If you encounter this frequently while using Claude Sonnet 4.5 or Opus 4.1, you can try Sonnet 4, which has different usage restrictions.", + "com_error_stream_expired": "The response stream has expired or already completed. Please try again.", "com_file_pages": "Pages: {{pages}}", "com_file_source": "File", "com_file_unknown": "Unknown File", diff --git a/client/src/utils/drafts.ts b/client/src/utils/drafts.ts index 1b3172def0..2e47c383b1 100644 --- a/client/src/utils/drafts.ts +++ b/client/src/utils/drafts.ts @@ -1,10 +1,17 @@ import debounce from 'lodash/debounce'; -import { LocalStorageKeys } from 'librechat-data-provider'; +import { Constants, LocalStorageKeys } from 'librechat-data-provider'; export const clearDraft = debounce((id?: string | null) => { localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`); }, 2500); +/** Synchronously removes both text and file drafts for a conversation (or NEW_CONVO fallback) */ +export const clearAllDrafts = (conversationId?: string | null) => { + const key = conversationId || Constants.NEW_CONVO; + localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${key}`); + localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${key}`); +}; + export const encodeBase64 = (plainText: string): string => { try { const textBytes = new TextEncoder().encode(plainText); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index e19c69e799..0c8c591488 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1616,6 +1616,10 @@ export enum ErrorTypes { * Model refused to respond (content policy violation) */ REFUSAL = 'refusal', + /** + * SSE stream 404 — job completed, expired, or was deleted before the subscriber connected + */ + STREAM_EXPIRED = 'stream_expired', } /** From b1899723817d02977f6194794e7db25f9fbbe13c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 14:52:06 -0400 Subject: [PATCH 066/111] =?UTF-8?q?=F0=9F=8E=AD=20fix:=20Set=20Explicit=20?= =?UTF-8?q?Permission=20Defaults=20for=20USER=20Role=20in=20roleDefaults?= =?UTF-8?q?=20(#12308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: set explicit permission defaults for USER role in roleDefaults Previously several permission types for the USER role had empty objects in roleDefaults, causing the getPermissionValue fallback to resolve SHARE/CREATE via the zod schema defaults on fresh installs. This silently granted users MCP server creation ability and left share permissions ambiguous. Sets explicit defaults for all multi-field permission types: - PROMPTS/AGENTS: USE and CREATE true, SHARE false - MCP_SERVERS: USE true, CREATE/SHARE false - REMOTE_AGENTS: all false Adds regression tests covering the exact reported scenarios (fresh install with `agents: { use: true }`, restart preserving admin-panel overrides) and structural guards against future permission schema expansions missing explicit USER defaults. Closes #12306. * fix: guard MCP_SERVERS.CREATE against configDefaults fallback + add migration The roleDefaults fix alone was insufficient: loadDefaultInterface propagates configDefaults.mcpServers.create=true as tier-1 in getPermissionValue, overriding the roleDefault of false. This commit: - Adds conditional guards for MCP_SERVERS.CREATE and REMOTE_AGENTS.CREATE matching the existing AGENTS/PROMPTS pattern (only include CREATE when explicitly configured in yaml OR on fresh install) - Uses raw interfaceConfig for MCP_SERVERS.CREATE tier-1 instead of loadedInterface (which includes configDefaults fallback) - Adds one-time migration backfill: corrects existing MCP_SERVERS.CREATE=true for USER role in DB when no explicit yaml config is present - Adds restart-scenario and migration regression tests for MCP_SERVERS - Cleans up roles.spec.ts: for..of loops, Permissions[] typing, Set for lookups, removes unnecessary aliases, improves JSDoc for exclusion list - Fixes misleading test name for agents regression test - Removes redundant not.toHaveProperty assertions after strict toEqual * fix: use raw interfaceConfig for REMOTE_AGENTS.CREATE tier-1 (consistency) Aligns REMOTE_AGENTS.CREATE with the MCP_SERVERS.CREATE fix — reads from raw interfaceConfig instead of loadedInterface to prevent a future configDefaults fallback from silently overriding the roleDefault. --- packages/api/src/app/permissions.spec.ts | 308 ++++++++++++++++++++++- packages/api/src/app/permissions.ts | 61 ++++- packages/data-provider/src/roles.spec.ts | 132 ++++++++++ packages/data-provider/src/roles.ts | 28 ++- 4 files changed, 510 insertions(+), 19 deletions(-) create mode 100644 packages/data-provider/src/roles.spec.ts diff --git a/packages/api/src/app/permissions.spec.ts b/packages/api/src/app/permissions.spec.ts index 7ab7e0d0d1..106ebbb50b 100644 --- a/packages/api/src/app/permissions.spec.ts +++ b/packages/api/src/app/permissions.spec.ts @@ -398,7 +398,7 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, [PermissionTypes.MCP_SERVERS]: { [Permissions.USE]: true, - [Permissions.CREATE]: true, + [Permissions.CREATE]: false, [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, @@ -555,7 +555,7 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, [PermissionTypes.MCP_SERVERS]: { [Permissions.USE]: true, - [Permissions.CREATE]: true, + [Permissions.CREATE]: false, [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, @@ -699,7 +699,7 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, [PermissionTypes.MCP_SERVERS]: { [Permissions.USE]: true, - [Permissions.CREATE]: true, + [Permissions.CREATE]: false, [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, @@ -848,7 +848,7 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, [PermissionTypes.MCP_SERVERS]: { [Permissions.USE]: true, - [Permissions.CREATE]: true, + [Permissions.CREATE]: false, [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, @@ -1002,7 +1002,7 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, [PermissionTypes.MCP_SERVERS]: { [Permissions.USE]: true, - [Permissions.CREATE]: true, + [Permissions.CREATE]: false, [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, @@ -2194,4 +2194,302 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE_PUBLIC]: false, }); }); + + it('should populate all default agent permissions on fresh install with object use config (regression: #12306)', async () => { + const config = { + interface: { + agents: { use: true }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + expect(userCall[1][PermissionTypes.AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + + expect(adminCall[1][PermissionTypes.AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }); + }); + + it('should preserve admin-panel changes to USER agents.CREATE across restart (regression: #12306 restart)', async () => { + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + }, + }); + + const config = { + interface: { + agents: { use: true }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1][PermissionTypes.AGENTS]).toEqual({ + [Permissions.USE]: true, + }); + }); + + it('should preserve all admin-panel changes when agents is not in yaml config (regression: #12306 restart)', async () => { + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + }, + }); + + const config = {}; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1]).not.toHaveProperty(PermissionTypes.AGENTS); + expect(userCall[1]).not.toHaveProperty(PermissionTypes.PROMPTS); + }); + + it('should not grant USER share for prompts when only use is configured (regression: #12306)', async () => { + const config = { + interface: { + prompts: { use: true }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + + expect(adminCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }); + }); + + it('should not grant USER create for mcpServers when only use is configured (regression: #12306)', async () => { + const config = { + interface: { + mcpServers: { use: true }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + expect(userCall[1][PermissionTypes.MCP_SERVERS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + + expect(adminCall[1][PermissionTypes.MCP_SERVERS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }); + }); + + it('should preserve existing MCP_SERVERS permissions on restart when mcpServers not in yaml config (regression: #12306 restart)', async () => { + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.MCP_SERVERS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + }, + }); + + const config = {}; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1][PermissionTypes.MCP_SERVERS]).toEqual({ + [Permissions.CREATE]: false, + }); + }); + + it('should migrate existing MCP_SERVERS.CREATE=true to false for USER when no explicit config (regression: #12306 migration)', async () => { + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.MCP_SERVERS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + }, + }); + + const config = {}; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + expect(userCall[1][PermissionTypes.MCP_SERVERS]).toEqual({ + [Permissions.CREATE]: false, + }); + expect(userCall[1]).not.toHaveProperty(PermissionTypes.AGENTS); + + expect(adminCall[1]).not.toHaveProperty(PermissionTypes.MCP_SERVERS); + expect(adminCall[1]).not.toHaveProperty(PermissionTypes.AGENTS); + }); + + it('should NOT migrate MCP_SERVERS.CREATE when yaml explicitly sets create: true (regression: #12306 migration)', async () => { + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.MCP_SERVERS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + }, + }); + + const config = { + interface: { + mcpServers: { use: true, create: true }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1][PermissionTypes.MCP_SERVERS][Permissions.CREATE]).toBe(true); + }); }); diff --git a/packages/api/src/app/permissions.ts b/packages/api/src/app/permissions.ts index 3638bdc0bb..5a557adfcf 100644 --- a/packages/api/src/app/permissions.ts +++ b/packages/api/src/app/permissions.ts @@ -352,11 +352,19 @@ export async function updateInterfacePermissions({ defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.USE], defaults.mcpServers?.use, ), - [Permissions.CREATE]: getPermissionValue( - loadedInterface.mcpServers?.create, - defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.CREATE], - defaults.mcpServers?.create, - ), + ...((typeof interfaceConfig?.mcpServers === 'object' && + 'create' in interfaceConfig.mcpServers) || + !existingPermissions?.[PermissionTypes.MCP_SERVERS] + ? { + [Permissions.CREATE]: getPermissionValue( + typeof interfaceConfig?.mcpServers === 'object' + ? interfaceConfig.mcpServers.create + : undefined, + defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.CREATE], + defaults.mcpServers?.create, + ), + } + : {}), ...((typeof interfaceConfig?.mcpServers === 'object' && ('share' in interfaceConfig.mcpServers || 'public' in interfaceConfig.mcpServers)) || !existingPermissions?.[PermissionTypes.MCP_SERVERS] @@ -380,11 +388,19 @@ export async function updateInterfacePermissions({ defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.USE], defaults.remoteAgents?.use, ), - [Permissions.CREATE]: getPermissionValue( - loadedInterface.remoteAgents?.create, - defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.CREATE], - defaults.remoteAgents?.create, - ), + ...((typeof interfaceConfig?.remoteAgents === 'object' && + 'create' in interfaceConfig.remoteAgents) || + !existingPermissions?.[PermissionTypes.REMOTE_AGENTS] + ? { + [Permissions.CREATE]: getPermissionValue( + typeof interfaceConfig?.remoteAgents === 'object' + ? interfaceConfig.remoteAgents.create + : undefined, + defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.CREATE], + defaults.remoteAgents?.create, + ), + } + : {}), ...((typeof interfaceConfig?.remoteAgents === 'object' && ('share' in interfaceConfig.remoteAgents || 'public' in interfaceConfig.remoteAgents)) || !existingPermissions?.[PermissionTypes.REMOTE_AGENTS] @@ -511,6 +527,31 @@ export async function updateInterfacePermissions({ } } + /** + * One-time migration: correct MCP_SERVERS.CREATE for USER role. + * Before the explicit roleDefaults fix, Zod schema defaults resolved CREATE to true + * for all roles. ADMIN should keep CREATE: true, but USER should have CREATE: false + * unless explicitly configured otherwise in librechat.yaml. + */ + if (roleName === SystemRoles.USER) { + const existingMcpPerms = existingPermissions?.[PermissionTypes.MCP_SERVERS]; + const mcpCreateExplicit = + typeof interfaceConfig?.mcpServers === 'object' && 'create' in interfaceConfig.mcpServers; + if ( + existingMcpPerms?.[Permissions.CREATE] === true && + !mcpCreateExplicit && + defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.CREATE] === false + ) { + logger.debug( + `Role '${roleName}': Migrating MCP_SERVERS.CREATE from true to false (Zod default correction)`, + ); + permissionsToUpdate[PermissionTypes.MCP_SERVERS] = { + ...permissionsToUpdate[PermissionTypes.MCP_SERVERS], + [Permissions.CREATE]: false, + }; + } + } + // Update permissions if any need updating if (Object.keys(permissionsToUpdate).length > 0) { await updateAccessPermissions(roleName, permissionsToUpdate, existingRole); diff --git a/packages/data-provider/src/roles.spec.ts b/packages/data-provider/src/roles.spec.ts new file mode 100644 index 0000000000..60dac5ab50 --- /dev/null +++ b/packages/data-provider/src/roles.spec.ts @@ -0,0 +1,132 @@ +import { Permissions, PermissionTypes, permissionsSchema } from './permissions'; +import { SystemRoles, roleDefaults } from './roles'; + +const RESOURCE_MANAGEMENT_FIELDS: Permissions[] = [ + Permissions.CREATE, + Permissions.SHARE, + Permissions.SHARE_PUBLIC, +]; + +/** + * Permission types where CREATE/SHARE/SHARE_PUBLIC must default to false for USER. + * MEMORIES is excluded: its CREATE/READ/UPDATE apply to the user's own private data. + * AGENTS/PROMPTS are excluded: CREATE=true is intentional (users own their agents/prompts). + * Add new types here if they gate shared/multi-user resources. + */ +const RESOURCE_PERMISSION_TYPES: PermissionTypes[] = [ + PermissionTypes.MCP_SERVERS, + PermissionTypes.REMOTE_AGENTS, +]; + +describe('roleDefaults', () => { + describe('USER role', () => { + const userPerms = roleDefaults[SystemRoles.USER].permissions; + + it('should have explicit values for every field in every multi-field permission type', () => { + const schemaShape = permissionsSchema.shape; + + for (const [permType, subSchema] of Object.entries(schemaShape)) { + const fieldNames = Object.keys(subSchema.shape); + if (fieldNames.length <= 1) { + continue; + } + + const userValues = + userPerms[permType as PermissionTypes] as Record; + + for (const field of fieldNames) { + expect({ + permType, + field, + value: userValues[field], + }).toEqual( + expect.objectContaining({ + permType, + field, + value: expect.any(Boolean), + }), + ); + } + } + }); + + it('should never grant CREATE, SHARE, or SHARE_PUBLIC by default for resource-management types', () => { + for (const permType of RESOURCE_PERMISSION_TYPES) { + const permissions = userPerms[permType] as Record; + for (const field of RESOURCE_MANAGEMENT_FIELDS) { + if (permissions[field] === undefined) { + continue; + } + expect({ + permType, + field, + value: permissions[field], + }).toEqual( + expect.objectContaining({ + permType, + field, + value: false, + }), + ); + } + } + }); + + it('should cover every permission type that has CREATE, SHARE, or SHARE_PUBLIC fields', () => { + const schemaShape = permissionsSchema.shape; + const restrictedSet = new Set(RESOURCE_PERMISSION_TYPES); + + for (const [permType, subSchema] of Object.entries(schemaShape)) { + const fieldNames = Object.keys(subSchema.shape); + const hasResourceFields = fieldNames.some((f) => RESOURCE_MANAGEMENT_FIELDS.includes(f as Permissions)); + if (!hasResourceFields) { + continue; + } + + const isTracked = + restrictedSet.has(permType) || + permType === PermissionTypes.MEMORIES || + permType === PermissionTypes.PROMPTS || + permType === PermissionTypes.AGENTS; + + expect({ + permType, + tracked: isTracked, + }).toEqual( + expect.objectContaining({ + permType, + tracked: true, + }), + ); + } + }); + }); + + describe('ADMIN role', () => { + const adminPerms = roleDefaults[SystemRoles.ADMIN].permissions; + + it('should have explicit values for every field in every permission type', () => { + const schemaShape = permissionsSchema.shape; + + for (const [permType, subSchema] of Object.entries(schemaShape)) { + const fieldNames = Object.keys(subSchema.shape); + const adminValues = + adminPerms[permType as PermissionTypes] as Record; + + for (const field of fieldNames) { + expect({ + permType, + field, + value: adminValues[field], + }).toEqual( + expect.objectContaining({ + permType, + field, + value: expect.any(Boolean), + }), + ); + } + } + }); + }); +}); diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index b494ee5817..1ba7a8cce2 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -180,10 +180,20 @@ export const roleDefaults = defaultRolesSchema.parse({ [SystemRoles.USER]: { name: SystemRoles.USER, permissions: { - [PermissionTypes.PROMPTS]: {}, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, [PermissionTypes.BOOKMARKS]: {}, [PermissionTypes.MEMORIES]: {}, - [PermissionTypes.AGENTS]: {}, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, [PermissionTypes.MULTI_CONVO]: {}, [PermissionTypes.TEMPORARY_CHAT]: {}, [PermissionTypes.RUN_CODE]: {}, @@ -198,8 +208,18 @@ export const roleDefaults = defaultRolesSchema.parse({ }, [PermissionTypes.FILE_SEARCH]: {}, [PermissionTypes.FILE_CITATIONS]: {}, - [PermissionTypes.MCP_SERVERS]: {}, - [PermissionTypes.REMOTE_AGENTS]: {}, + [PermissionTypes.MCP_SERVERS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }, }, }); From 93952f06b42e35fe98069de6bc5e2621379c27e9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 15:15:10 -0400 Subject: [PATCH 067/111] =?UTF-8?q?=F0=9F=A7=AF=20fix:=20Remove=20Revoked?= =?UTF-8?q?=20Agents=20from=20User=20Favorites=20(#12296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🧯 fix: Remove revoked agents from user favorites When agent access is revoked, the agent remained in the user's favorites causing repeated 403 errors on page load. Backend now cleans up favorites on permission revocation; frontend treats 403 like 404 and auto-removes stale agent references. * 🧪 fix: Address review findings for stale agent favorites cleanup - Guard cleanup effect with ref to prevent infinite loop on mutation failure (Finding 1) - Use validated results.revoked instead of raw request payload for revokedUserIds (Finding 3) - Stabilize staleAgentIds memo with string key to avoid spurious re-evaluation during drag-drop (Finding 5) - Add JSDoc with param types to removeRevokedAgentFromFavorites (Finding 7) - Return promise from removeRevokedAgentFromFavorites for testability - Add 7 backend tests covering revocation cleanup paths - Add 3 frontend tests for 403 handling and stale cleanup persistence --- .../controllers/PermissionsController.js | 34 ++- .../__tests__/PermissionsController.spec.js | 268 ++++++++++++++++++ .../Nav/Favorites/FavoritesList.tsx | 29 +- .../Favorites/tests/FavoritesList.spec.tsx | 81 ++++++ 4 files changed, 409 insertions(+), 3 deletions(-) create mode 100644 api/server/controllers/__tests__/PermissionsController.spec.js diff --git a/api/server/controllers/PermissionsController.js b/api/server/controllers/PermissionsController.js index 51993d083c..16930c5139 100644 --- a/api/server/controllers/PermissionsController.js +++ b/api/server/controllers/PermissionsController.js @@ -24,7 +24,7 @@ const { entraIdPrincipalFeatureEnabled, searchEntraIdPrincipals, } = require('~/server/services/GraphApiService'); -const { AclEntry, AccessRole } = require('~/db/models'); +const { Agent, AclEntry, AccessRole, User } = require('~/db/models'); /** * Generic controller for resource permission endpoints @@ -43,6 +43,28 @@ const validateResourceType = (resourceType) => { } }; +/** + * Removes an agent from the favorites of specified users (fire-and-forget). + * Both AGENT and REMOTE_AGENT resource types share the Agent collection. + * @param {string} resourceId - The agent's MongoDB ObjectId hex string + * @param {string[]} userIds - User ObjectId strings whose favorites should be cleaned + */ +const removeRevokedAgentFromFavorites = (resourceId, userIds) => + Agent.findOne({ _id: resourceId }, { id: 1 }) + .lean() + .then((agent) => { + if (!agent) { + return; + } + return User.updateMany( + { _id: { $in: userIds }, 'favorites.agentId': agent.id }, + { $pull: { favorites: { agentId: agent.id } } }, + ); + }) + .catch((err) => { + logger.error('[removeRevokedAgentFromFavorites] Error cleaning up favorites', err); + }); + /** * Bulk update permissions for a resource (grant, update, remove) * @route PUT /api/{resourceType}/{resourceId}/permissions @@ -155,6 +177,16 @@ const updateResourcePermissions = async (req, res) => { grantedBy: userId, }); + const isAgentResource = + resourceType === ResourceType.AGENT || resourceType === ResourceType.REMOTE_AGENT; + const revokedUserIds = results.revoked + .filter((p) => p.type === PrincipalType.USER && p.id) + .map((p) => p.id); + + if (isAgentResource && revokedUserIds.length > 0) { + removeRevokedAgentFromFavorites(resourceId, revokedUserIds); + } + /** @type {TUpdateResourcePermissionsResponse} */ const response = { message: 'Permissions updated successfully', diff --git a/api/server/controllers/__tests__/PermissionsController.spec.js b/api/server/controllers/__tests__/PermissionsController.spec.js new file mode 100644 index 0000000000..840eaf0c30 --- /dev/null +++ b/api/server/controllers/__tests__/PermissionsController.spec.js @@ -0,0 +1,268 @@ +const mongoose = require('mongoose'); + +const mockLogger = { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() }; + +jest.mock('@librechat/data-schemas', () => ({ + logger: mockLogger, +})); + +const { ResourceType, PrincipalType } = jest.requireActual('librechat-data-provider'); + +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), +})); + +jest.mock('@librechat/api', () => ({ + enrichRemoteAgentPrincipals: jest.fn(), + backfillRemoteAgentPermissions: jest.fn(), +})); + +const mockBulkUpdateResourcePermissions = jest.fn(); + +jest.mock('~/server/services/PermissionService', () => ({ + bulkUpdateResourcePermissions: (...args) => mockBulkUpdateResourcePermissions(...args), + ensureGroupPrincipalExists: jest.fn(), + getEffectivePermissions: jest.fn(), + ensurePrincipalExists: jest.fn(), + getAvailableRoles: jest.fn(), + findAccessibleResources: jest.fn(), + getResourcePermissionsMap: jest.fn(), +})); + +jest.mock('~/models', () => ({ + searchPrincipals: jest.fn(), + sortPrincipalsByRelevance: jest.fn(), + calculateRelevanceScore: jest.fn(), +})); + +jest.mock('~/server/services/GraphApiService', () => ({ + entraIdPrincipalFeatureEnabled: jest.fn(() => false), + searchEntraIdPrincipals: jest.fn(), +})); + +const mockAgentFindOne = jest.fn(); +const mockUserUpdateMany = jest.fn(); + +jest.mock('~/db/models', () => ({ + Agent: { + findOne: (...args) => mockAgentFindOne(...args), + }, + AclEntry: {}, + AccessRole: {}, + User: { + updateMany: (...args) => mockUserUpdateMany(...args), + }, +})); + +const { updateResourcePermissions } = require('../PermissionsController'); + +const createMockReq = (overrides = {}) => ({ + params: { resourceType: ResourceType.AGENT, resourceId: '507f1f77bcf86cd799439011' }, + body: { updated: [], removed: [], public: false }, + user: { id: 'user-1', role: 'USER' }, + headers: { authorization: '' }, + ...overrides, +}); + +const createMockRes = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +}; + +const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); + +describe('PermissionsController', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('updateResourcePermissions — favorites cleanup', () => { + const agentObjectId = new mongoose.Types.ObjectId().toString(); + const revokedUserId = new mongoose.Types.ObjectId().toString(); + + beforeEach(() => { + mockBulkUpdateResourcePermissions.mockResolvedValue({ + granted: [], + updated: [], + revoked: [{ type: PrincipalType.USER, id: revokedUserId, name: 'Revoked User' }], + errors: [], + }); + + mockAgentFindOne.mockReturnValue({ + lean: () => Promise.resolve({ _id: agentObjectId, id: 'agent_abc123' }), + }); + mockUserUpdateMany.mockResolvedValue({ modifiedCount: 1 }); + }); + + it('removes agent from revoked users favorites on AGENT resource type', async () => { + const req = createMockReq({ + params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId }, + body: { + updated: [], + removed: [{ type: PrincipalType.USER, id: revokedUserId }], + public: false, + }, + }); + const res = createMockRes(); + + await updateResourcePermissions(req, res); + await flushPromises(); + + expect(res.status).toHaveBeenCalledWith(200); + expect(mockAgentFindOne).toHaveBeenCalledWith({ _id: agentObjectId }, { id: 1 }); + expect(mockUserUpdateMany).toHaveBeenCalledWith( + { _id: { $in: [revokedUserId] }, 'favorites.agentId': 'agent_abc123' }, + { $pull: { favorites: { agentId: 'agent_abc123' } } }, + ); + }); + + it('removes agent from revoked users favorites on REMOTE_AGENT resource type', async () => { + const req = createMockReq({ + params: { resourceType: ResourceType.REMOTE_AGENT, resourceId: agentObjectId }, + body: { + updated: [], + removed: [{ type: PrincipalType.USER, id: revokedUserId }], + public: false, + }, + }); + const res = createMockRes(); + + await updateResourcePermissions(req, res); + await flushPromises(); + + expect(mockAgentFindOne).toHaveBeenCalledWith({ _id: agentObjectId }, { id: 1 }); + expect(mockUserUpdateMany).toHaveBeenCalled(); + }); + + it('uses results.revoked (validated) not raw request payload', async () => { + const validId = new mongoose.Types.ObjectId().toString(); + const invalidId = 'not-a-valid-id'; + + mockBulkUpdateResourcePermissions.mockResolvedValue({ + granted: [], + updated: [], + revoked: [{ type: PrincipalType.USER, id: validId }], + errors: [{ principal: { type: PrincipalType.USER, id: invalidId }, error: 'Invalid ID' }], + }); + + const req = createMockReq({ + params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId }, + body: { + updated: [], + removed: [ + { type: PrincipalType.USER, id: validId }, + { type: PrincipalType.USER, id: invalidId }, + ], + public: false, + }, + }); + const res = createMockRes(); + + await updateResourcePermissions(req, res); + await flushPromises(); + + expect(mockUserUpdateMany).toHaveBeenCalledWith( + expect.objectContaining({ _id: { $in: [validId] } }), + expect.any(Object), + ); + }); + + it('skips cleanup when no USER principals are revoked', async () => { + mockBulkUpdateResourcePermissions.mockResolvedValue({ + granted: [], + updated: [], + revoked: [{ type: PrincipalType.GROUP, id: 'group-1' }], + errors: [], + }); + + const req = createMockReq({ + params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId }, + body: { + updated: [], + removed: [{ type: PrincipalType.GROUP, id: 'group-1' }], + public: false, + }, + }); + const res = createMockRes(); + + await updateResourcePermissions(req, res); + await flushPromises(); + + expect(mockAgentFindOne).not.toHaveBeenCalled(); + expect(mockUserUpdateMany).not.toHaveBeenCalled(); + }); + + it('skips cleanup for non-agent resource types', async () => { + mockBulkUpdateResourcePermissions.mockResolvedValue({ + granted: [], + updated: [], + revoked: [{ type: PrincipalType.USER, id: revokedUserId }], + errors: [], + }); + + const req = createMockReq({ + params: { resourceType: ResourceType.PROMPTGROUP, resourceId: agentObjectId }, + body: { + updated: [], + removed: [{ type: PrincipalType.USER, id: revokedUserId }], + public: false, + }, + }); + const res = createMockRes(); + + await updateResourcePermissions(req, res); + await flushPromises(); + + expect(res.status).toHaveBeenCalledWith(200); + expect(mockAgentFindOne).not.toHaveBeenCalled(); + }); + + it('handles agent not found gracefully', async () => { + mockAgentFindOne.mockReturnValue({ + lean: () => Promise.resolve(null), + }); + + const req = createMockReq({ + params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId }, + body: { + updated: [], + removed: [{ type: PrincipalType.USER, id: revokedUserId }], + public: false, + }, + }); + const res = createMockRes(); + + await updateResourcePermissions(req, res); + await flushPromises(); + + expect(mockAgentFindOne).toHaveBeenCalled(); + expect(mockUserUpdateMany).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('logs error when User.updateMany fails without blocking response', async () => { + mockUserUpdateMany.mockRejectedValue(new Error('DB connection lost')); + + const req = createMockReq({ + params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId }, + body: { + updated: [], + removed: [{ type: PrincipalType.USER, id: revokedUserId }], + public: false, + }, + }); + const res = createMockRes(); + + await updateResourcePermissions(req, res); + await flushPromises(); + + expect(res.status).toHaveBeenCalledWith(200); + expect(mockLogger.error).toHaveBeenCalledWith( + '[removeRevokedAgentFromFavorites] Error cleaning up favorites', + expect.any(Error), + ); + }); + }); +}); diff --git a/client/src/components/Nav/Favorites/FavoritesList.tsx b/client/src/components/Nav/Favorites/FavoritesList.tsx index 82225733fd..0ca23f8853 100644 --- a/client/src/components/Nav/Favorites/FavoritesList.tsx +++ b/client/src/components/Nav/Favorites/FavoritesList.tsx @@ -198,7 +198,8 @@ export default function FavoritesList({ } catch (error) { if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { response?: { status?: number } }; - if (axiosError.response?.status === 404) { + const status = axiosError.response?.status; + if (status === 404 || status === 403) { return { found: false }; } } @@ -206,10 +207,34 @@ export default function FavoritesList({ } }, staleTime: 1000 * 60 * 5, - enabled: missingAgentIds.length > 0, })), }); + const staleAgentIdsKey = useMemo(() => { + const ids: string[] = []; + for (let i = 0; i < missingAgentIds.length; i++) { + const query = missingAgentQueries[i]; + if (query.data && !query.data.found) { + ids.push(missingAgentIds[i]); + } + } + return ids.sort().join(','); + }, [missingAgentIds, missingAgentQueries]); + + const cleanupAttemptedRef = useRef(''); + + useEffect(() => { + if (!staleAgentIdsKey || cleanupAttemptedRef.current === staleAgentIdsKey) { + return; + } + const staleSet = new Set(staleAgentIdsKey.split(',')); + const cleaned = safeFavorites.filter((f) => !f.agentId || !staleSet.has(f.agentId)); + if (cleaned.length < safeFavorites.length) { + cleanupAttemptedRef.current = staleAgentIdsKey; + reorderFavorites(cleaned, true); + } + }, [staleAgentIdsKey, safeFavorites, reorderFavorites]); + const combinedAgentsMap = useMemo(() => { if (agentsMap === undefined) { return undefined; diff --git a/client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx b/client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx index ed71221de3..74228dc169 100644 --- a/client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx +++ b/client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx @@ -188,5 +188,86 @@ describe('FavoritesList', () => { // No favorite items should be rendered (deleted agent is filtered out) expect(queryAllByTestId('favorite-item')).toHaveLength(0); }); + + it('should treat 403 the same as 404 — agent not rendered', async () => { + const validAgent: t.Agent = { + id: 'valid-agent', + name: 'Valid Agent', + author: 'test-author', + } as t.Agent; + + mockFavorites.push({ agentId: 'valid-agent' }, { agentId: 'revoked-agent' }); + + (dataService.getAgentById as jest.Mock).mockImplementation( + ({ agent_id }: { agent_id: string }) => { + if (agent_id === 'valid-agent') { + return Promise.resolve(validAgent); + } + if (agent_id === 'revoked-agent') { + return Promise.reject({ response: { status: 403 } }); + } + return Promise.reject(new Error('Unknown agent')); + }, + ); + + const { findAllByTestId } = renderWithProviders(); + + const favoriteItems = await findAllByTestId('favorite-item'); + expect(favoriteItems).toHaveLength(1); + expect(favoriteItems[0]).toHaveTextContent('Valid Agent'); + }); + + it('should call reorderFavorites to persist removal of stale agents', async () => { + const mockReorderFavorites = jest.fn().mockResolvedValue(undefined); + mockUseFavorites.mockReturnValue({ + favorites: [{ agentId: 'revoked-agent' }], + reorderFavorites: mockReorderFavorites, + isLoading: false, + }); + + (dataService.getAgentById as jest.Mock).mockRejectedValue({ response: { status: 403 } }); + + renderWithProviders(); + + await waitFor(() => { + expect(mockReorderFavorites).toHaveBeenCalledWith([], true); + }); + }); + + it('should only attempt cleanup once even when favorites revert to stale state', async () => { + const mockReorderFavorites = jest.fn().mockResolvedValue(undefined); + + mockUseFavorites.mockReturnValue({ + favorites: [{ agentId: 'revoked-agent' }], + reorderFavorites: mockReorderFavorites, + isLoading: false, + }); + + (dataService.getAgentById as jest.Mock).mockRejectedValue({ response: { status: 403 } }); + + const { rerender } = renderWithProviders(); + + await waitFor(() => { + expect(mockReorderFavorites).toHaveBeenCalledWith([], true); + }); + + expect(mockReorderFavorites).toHaveBeenCalledTimes(1); + + rerender( + + + + + + + + + , + ); + + await new Promise((r) => setTimeout(r, 50)); + + expect(mockReorderFavorites).toHaveBeenCalledTimes(1); + }); }); }); From a88bfae4dd6cb88811f2b77deb82edf5233a412f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 15:33:46 -0400 Subject: [PATCH 068/111] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20fix:=20Correct?= =?UTF-8?q?=20ToolMessage=20Response=20Format=20for=20Agent-Mode=20Image?= =?UTF-8?q?=20Tools=20(#12310)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Set response format for agent tools in DALLE3, FluxAPI, and StableDiffusion classes - Added logic to set `responseFormat` to 'content_and_artifact' when `isAgent` is true in DALLE3.js, FluxAPI.js, and StableDiffusion.js. * test: Add regression tests for image tool agent mode in imageTools-agent.spec.js - Introduced a new test suite for DALLE3, FluxAPI, and StableDiffusion classes to verify that the invoke() method returns a ToolMessage with base64 in artifact.content, ensuring it is not serialized into content. - Validated that responseFormat is set to 'content_and_artifact' when isAgent is true, and confirmed the correct handling of base64 data in the response. * fix: handle agent error paths and generateFinetunedImage in image tools - StableDiffusion._call() was returning a raw string on API error, bypassing returnValue() and breaking the content_and_artifact contract when isAgent is true - FluxAPI.generateFinetunedImage() had no isAgent branch; it would call processFileURL (unset in agent context) instead of fetching and returning the base64 image as an artifact tuple - Add JSDoc to all three responseFormat assignments clarifying why LangChain requires this property for correct ToolMessage construction * test: expand image tool agent mode regression suite - Add env var save/restore in beforeEach/afterEach to prevent test pollution - Add error path tests for all three tools verifying ToolMessage content and artifact are correctly populated when the upstream API fails - Add generate_finetuned action test for FluxAPI covering the new agent branch in generateFinetunedImage * chore: fix lint errors in FluxAPI and imageTools-agent spec * chore: fix import ordering in imageTools-agent spec --- api/app/clients/tools/structured/DALLE3.js | 4 + api/app/clients/tools/structured/FluxAPI.js | 42 ++- .../tools/structured/StableDiffusion.js | 6 +- .../structured/specs/imageTools-agent.spec.js | 294 ++++++++++++++++++ 4 files changed, 338 insertions(+), 8 deletions(-) create mode 100644 api/app/clients/tools/structured/specs/imageTools-agent.spec.js diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index 26610f73ba..c48db1d764 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -51,6 +51,10 @@ class DALLE3 extends Tool { this.fileStrategy = fields.fileStrategy; /** @type {boolean} */ this.isAgent = fields.isAgent; + if (this.isAgent) { + /** Ensures LangChain maps [content, artifact] tuple to ToolMessage fields instead of serializing it into content. */ + this.responseFormat = 'content_and_artifact'; + } if (fields.processFileURL) { /** @type {processFileURL} Necessary for output to contain all image metadata. */ this.processFileURL = fields.processFileURL.bind(this); diff --git a/api/app/clients/tools/structured/FluxAPI.js b/api/app/clients/tools/structured/FluxAPI.js index 56f86a707d..f8341f7904 100644 --- a/api/app/clients/tools/structured/FluxAPI.js +++ b/api/app/clients/tools/structured/FluxAPI.js @@ -113,6 +113,10 @@ class FluxAPI extends Tool { /** @type {boolean} **/ this.isAgent = fields.isAgent; + if (this.isAgent) { + /** Ensures LangChain maps [content, artifact] tuple to ToolMessage fields instead of serializing it into content. */ + this.responseFormat = 'content_and_artifact'; + } this.returnMetadata = fields.returnMetadata ?? false; if (fields.processFileURL) { @@ -524,10 +528,40 @@ class FluxAPI extends Tool { return this.returnValue('No image data received from Flux API.'); } - // Try saving the image locally const imageUrl = resultData.sample; const imageName = `img-${uuidv4()}.png`; + if (this.isAgent) { + try { + const fetchOptions = {}; + if (process.env.PROXY) { + fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY); + } + const imageResponse = await fetch(imageUrl, fetchOptions); + const arrayBuffer = await imageResponse.arrayBuffer(); + const base64 = Buffer.from(arrayBuffer).toString('base64'); + const content = [ + { + type: ContentTypes.IMAGE_URL, + image_url: { + url: `data:image/png;base64,${base64}`, + }, + }, + ]; + + const response = [ + { + type: ContentTypes.TEXT, + text: displayMessage, + }, + ]; + return [response, { content }]; + } catch (error) { + logger.error('[FluxAPI] Error processing finetuned image for agent:', error); + return this.returnValue(`Failed to process the finetuned image. ${error.message}`); + } + } + try { logger.debug('[FluxAPI] Saving finetuned image:', imageUrl); const result = await this.processFileURL({ @@ -541,12 +575,6 @@ class FluxAPI extends Tool { logger.debug('[FluxAPI] Finetuned image saved to path:', result.filepath); - // Calculate cost based on endpoint - const endpointKey = endpoint.includes('ultra') - ? 'FLUX_PRO_1_1_ULTRA_FINETUNED' - : 'FLUX_PRO_FINETUNED'; - const cost = FluxAPI.PRICING[endpointKey] || 0; - // Return the result based on returnMetadata flag this.result = this.returnMetadata ? result : this.wrapInMarkdown(result.filepath); return this.returnValue(this.result); } catch (error) { diff --git a/api/app/clients/tools/structured/StableDiffusion.js b/api/app/clients/tools/structured/StableDiffusion.js index d7a7a4d96b..8cf4b141bb 100644 --- a/api/app/clients/tools/structured/StableDiffusion.js +++ b/api/app/clients/tools/structured/StableDiffusion.js @@ -43,6 +43,10 @@ class StableDiffusionAPI extends Tool { this.returnMetadata = fields.returnMetadata ?? false; /** @type {boolean} */ this.isAgent = fields.isAgent; + if (this.isAgent) { + /** Ensures LangChain maps [content, artifact] tuple to ToolMessage fields instead of serializing it into content. */ + this.responseFormat = 'content_and_artifact'; + } if (fields.uploadImageBuffer) { /** @type {uploadImageBuffer} Necessary for output to contain all image metadata. */ this.uploadImageBuffer = fields.uploadImageBuffer.bind(this); @@ -115,7 +119,7 @@ class StableDiffusionAPI extends Tool { generationResponse = await axios.post(`${url}/sdapi/v1/txt2img`, payload); } catch (error) { logger.error('[StableDiffusion] Error while generating image:', error); - return 'Error making API request.'; + return this.returnValue('Error making API request.'); } const image = generationResponse.data.images[0]; diff --git a/api/app/clients/tools/structured/specs/imageTools-agent.spec.js b/api/app/clients/tools/structured/specs/imageTools-agent.spec.js new file mode 100644 index 0000000000..b82dd87b3f --- /dev/null +++ b/api/app/clients/tools/structured/specs/imageTools-agent.spec.js @@ -0,0 +1,294 @@ +/** + * Regression tests for image tool agent mode — verifies that invoke() returns + * a ToolMessage with base64 in artifact.content rather than serialized into content. + * + * Root cause: DALLE3/FluxAPI/StableDiffusion extend LangChain's Tool but did not + * set responseFormat = 'content_and_artifact'. LangChain's invoke() would then + * JSON.stringify the entire [content, artifact] tuple into ToolMessage.content, + * dumping base64 into token counting and causing context exhaustion. + */ + +const axios = require('axios'); +const OpenAI = require('openai'); +const undici = require('undici'); +const fetch = require('node-fetch'); +const { ToolMessage } = require('@langchain/core/messages'); +const { ContentTypes } = require('librechat-data-provider'); +const StableDiffusionAPI = require('../StableDiffusion'); +const FluxAPI = require('../FluxAPI'); +const DALLE3 = require('../DALLE3'); + +jest.mock('axios'); +jest.mock('openai'); +jest.mock('node-fetch'); +jest.mock('undici', () => ({ + ProxyAgent: jest.fn(), + fetch: jest.fn(), +})); +jest.mock('@librechat/data-schemas', () => ({ + logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() }, +})); +jest.mock('path', () => ({ + resolve: jest.fn(), + join: jest.fn().mockReturnValue('/mock/path'), + relative: jest.fn().mockReturnValue('relative/path'), + extname: jest.fn().mockReturnValue('.png'), +})); +jest.mock('fs', () => ({ + existsSync: jest.fn().mockReturnValue(true), + mkdirSync: jest.fn(), + promises: { writeFile: jest.fn(), readFile: jest.fn(), unlink: jest.fn() }, +})); + +const FAKE_BASE64 = 'aGVsbG8='; + +const makeToolCall = (name, args) => ({ + id: 'call_test_123', + name, + args, + type: 'tool_call', +}); + +describe('image tools - agent mode ToolMessage format', () => { + const ENV_KEYS = ['DALLE_API_KEY', 'FLUX_API_KEY', 'SD_WEBUI_URL', 'PROXY']; + let savedEnv = {}; + + beforeEach(() => { + jest.clearAllMocks(); + for (const key of ENV_KEYS) { + savedEnv[key] = process.env[key]; + } + process.env.DALLE_API_KEY = 'test-dalle-key'; + process.env.FLUX_API_KEY = 'test-flux-key'; + process.env.SD_WEBUI_URL = 'http://localhost:7860'; + delete process.env.PROXY; + }); + + afterEach(() => { + for (const key of ENV_KEYS) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + savedEnv = {}; + }); + + describe('DALLE3', () => { + beforeEach(() => { + OpenAI.mockImplementation(() => ({ + images: { + generate: jest.fn().mockResolvedValue({ + data: [{ url: 'https://example.com/image.png' }], + }), + }, + })); + undici.fetch.mockResolvedValue({ + arrayBuffer: () => Promise.resolve(Buffer.from(FAKE_BASE64, 'base64')), + }); + }); + + it('sets responseFormat to content_and_artifact when isAgent is true', () => { + const dalle = new DALLE3({ isAgent: true }); + expect(dalle.responseFormat).toBe('content_and_artifact'); + }); + + it('does not set responseFormat when isAgent is false', () => { + const dalle = new DALLE3({ isAgent: false, processFileURL: jest.fn() }); + expect(dalle.responseFormat).not.toBe('content_and_artifact'); + }); + + it('invoke() returns ToolMessage with base64 in artifact, not serialized in content', async () => { + const dalle = new DALLE3({ isAgent: true }); + const result = await dalle.invoke( + makeToolCall('dalle', { + prompt: 'a box', + quality: 'standard', + size: '1024x1024', + style: 'vivid', + }), + ); + + expect(result).toBeInstanceOf(ToolMessage); + + const contentStr = + typeof result.content === 'string' ? result.content : JSON.stringify(result.content); + expect(contentStr).not.toContain(FAKE_BASE64); + + expect(result.artifact).toBeDefined(); + const artifactContent = result.artifact?.content; + expect(Array.isArray(artifactContent)).toBe(true); + expect(artifactContent[0].type).toBe(ContentTypes.IMAGE_URL); + expect(artifactContent[0].image_url.url).toContain('base64'); + }); + + it('invoke() returns ToolMessage with error string in content when API fails', async () => { + OpenAI.mockImplementation(() => ({ + images: { generate: jest.fn().mockRejectedValue(new Error('API error')) }, + })); + + const dalle = new DALLE3({ isAgent: true }); + const result = await dalle.invoke( + makeToolCall('dalle', { + prompt: 'a box', + quality: 'standard', + size: '1024x1024', + style: 'vivid', + }), + ); + + expect(result).toBeInstanceOf(ToolMessage); + const contentStr = + typeof result.content === 'string' ? result.content : JSON.stringify(result.content); + expect(contentStr).toContain('Something went wrong'); + expect(result.artifact).toBeDefined(); + }); + }); + + describe('FluxAPI', () => { + beforeEach(() => { + jest.useFakeTimers(); + axios.post.mockResolvedValue({ data: { id: 'task-123' } }); + axios.get.mockResolvedValue({ + data: { status: 'Ready', result: { sample: 'https://example.com/image.png' } }, + }); + fetch.mockResolvedValue({ + arrayBuffer: () => Promise.resolve(Buffer.from(FAKE_BASE64, 'base64')), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('sets responseFormat to content_and_artifact when isAgent is true', () => { + const flux = new FluxAPI({ isAgent: true }); + expect(flux.responseFormat).toBe('content_and_artifact'); + }); + + it('does not set responseFormat when isAgent is false', () => { + const flux = new FluxAPI({ isAgent: false, processFileURL: jest.fn() }); + expect(flux.responseFormat).not.toBe('content_and_artifact'); + }); + + it('invoke() returns ToolMessage with base64 in artifact, not serialized in content', async () => { + const flux = new FluxAPI({ isAgent: true }); + const invokePromise = flux.invoke( + makeToolCall('flux', { prompt: 'a box', endpoint: '/v1/flux-dev' }), + ); + await jest.runAllTimersAsync(); + const result = await invokePromise; + + expect(result).toBeInstanceOf(ToolMessage); + const contentStr = + typeof result.content === 'string' ? result.content : JSON.stringify(result.content); + expect(contentStr).not.toContain(FAKE_BASE64); + + expect(result.artifact).toBeDefined(); + const artifactContent = result.artifact?.content; + expect(Array.isArray(artifactContent)).toBe(true); + expect(artifactContent[0].type).toBe(ContentTypes.IMAGE_URL); + expect(artifactContent[0].image_url.url).toContain('base64'); + }); + + it('invoke() returns ToolMessage with base64 in artifact for generate_finetuned action', async () => { + const flux = new FluxAPI({ isAgent: true }); + const invokePromise = flux.invoke( + makeToolCall('flux', { + action: 'generate_finetuned', + prompt: 'a box', + finetune_id: 'ft-abc123', + endpoint: '/v1/flux-pro-finetuned', + }), + ); + await jest.runAllTimersAsync(); + const result = await invokePromise; + + expect(result).toBeInstanceOf(ToolMessage); + const contentStr = + typeof result.content === 'string' ? result.content : JSON.stringify(result.content); + expect(contentStr).not.toContain(FAKE_BASE64); + + expect(result.artifact).toBeDefined(); + const artifactContent = result.artifact?.content; + expect(Array.isArray(artifactContent)).toBe(true); + expect(artifactContent[0].type).toBe(ContentTypes.IMAGE_URL); + expect(artifactContent[0].image_url.url).toContain('base64'); + }); + + it('invoke() returns ToolMessage with error string in content when task submission fails', async () => { + axios.post.mockRejectedValue(new Error('Network error')); + + const flux = new FluxAPI({ isAgent: true }); + const invokePromise = flux.invoke( + makeToolCall('flux', { prompt: 'a box', endpoint: '/v1/flux-dev' }), + ); + await jest.runAllTimersAsync(); + const result = await invokePromise; + + expect(result).toBeInstanceOf(ToolMessage); + const contentStr = + typeof result.content === 'string' ? result.content : JSON.stringify(result.content); + expect(contentStr).toContain('Something went wrong'); + expect(result.artifact).toBeDefined(); + }); + }); + + describe('StableDiffusion', () => { + beforeEach(() => { + axios.post.mockResolvedValue({ + data: { + images: [FAKE_BASE64], + info: JSON.stringify({ height: 1024, width: 1024, seed: 42, infotexts: [] }), + }, + }); + }); + + it('sets responseFormat to content_and_artifact when isAgent is true', () => { + const sd = new StableDiffusionAPI({ isAgent: true, override: true }); + expect(sd.responseFormat).toBe('content_and_artifact'); + }); + + it('does not set responseFormat when isAgent is false', () => { + const sd = new StableDiffusionAPI({ + isAgent: false, + override: true, + uploadImageBuffer: jest.fn(), + }); + expect(sd.responseFormat).not.toBe('content_and_artifact'); + }); + + it('invoke() returns ToolMessage with base64 in artifact, not serialized in content', async () => { + const sd = new StableDiffusionAPI({ isAgent: true, override: true, userId: 'user-1' }); + const result = await sd.invoke( + makeToolCall('stable-diffusion', { prompt: 'a box', negative_prompt: '' }), + ); + + expect(result).toBeInstanceOf(ToolMessage); + const contentStr = + typeof result.content === 'string' ? result.content : JSON.stringify(result.content); + expect(contentStr).not.toContain(FAKE_BASE64); + + expect(result.artifact).toBeDefined(); + const artifactContent = result.artifact?.content; + expect(Array.isArray(artifactContent)).toBe(true); + expect(artifactContent[0].type).toBe(ContentTypes.IMAGE_URL); + expect(artifactContent[0].image_url.url).toContain('base64'); + }); + + it('invoke() returns ToolMessage with error string in content when API fails', async () => { + axios.post.mockRejectedValue(new Error('Connection refused')); + + const sd = new StableDiffusionAPI({ isAgent: true, override: true, userId: 'user-1' }); + const result = await sd.invoke( + makeToolCall('stable-diffusion', { prompt: 'a box', negative_prompt: '' }), + ); + + expect(result).toBeInstanceOf(ToolMessage); + const contentStr = + typeof result.content === 'string' ? result.content : JSON.stringify(result.content); + expect(contentStr).toContain('Error making API request'); + }); + }); +}); From 7e74165c3cc6841253378b2ebde1057caa13d80c Mon Sep 17 00:00:00 2001 From: Pol Burkardt Freire Date: Thu, 19 Mar 2026 20:49:52 +0100 Subject: [PATCH 069/111] =?UTF-8?q?=F0=9F=93=96=20feat:=20Add=20Native=20O?= =?UTF-8?q?DT=20Document=20Parser=20Support=20(#12303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add ODT support to native document parser * fix: replace execSync with jszip for ODT parsing * docs: update documentParserMimeTypes comment to include odt * fix: improve ODT XML extraction and add empty.odt fixture - Scope extraction to to exclude metadata/style nodes - Map and closings to newlines, preserving paragraph structure instead of collapsing everything to a single line - Handle as explicit newlines - Strip remaining tags, normalize horizontal whitespace, cap consecutive blank lines at one - Regenerate sample.odt as a two-paragraph fixture so the test exercises multi-paragraph output - Add empty.odt fixture and test asserting 'No text found in document' * fix: address review findings in ODT parser - Use static `import JSZip from 'jszip'` instead of dynamic import; jszip is CommonJS-only with no ESM/Jest-isolation concern (F1) - Decode the five standard XML entities after tag-stripping so documents with &, <, >, ", ' send correct text to the LLM (F2) - Remove @types/jszip devDependency; jszip ships bundled declarations and @types/jszip is a stale 2020 stub that would shadow them (F3) - Handle → \t and → ' ' before the generic tag stripper so tab-aligned and multi-space content is preserved (F4) - Add sample-entities.odt fixture and test covering entity decoding, tab, and spacing-element handling (F5) - Rename 'throws for empty odt' → 'throws for odt with no extractable text' to distinguish from a zero-byte/corrupt file case (F8) * fix: add decompressed content size cap to odtToText (F6) Reads uncompressed entry sizes from the JSZip internal metadata before extracting any content. Throws if the total exceeds 50MB, preventing a crafted ODT with a high-ratio compressed payload from exhausting heap. Adds a corresponding test using a real DEFLATE-compressed ZIP (~51KB on disk, 51MB uncompressed) to verify the guard fires before any extraction. * fix: add java to codeTypeMapping for file upload support .java files were rejected with "Unable to determine file type" because browsers send an empty MIME type for them and codeTypeMapping had no 'java' entry for inferMimeType() to fall back on. text/x-java was already present in all five validation lists (fullMimeTypesList, codeInterpreterMimeTypesList, retrievalMimeTypesList, textMimeTypes, retrievalMimeTypes), so mapping to it (not text/plain) ensures .java uploads work for both File Search and Code Interpreter. Closes #12307 * fix: address follow-up review findings (A-E) A: regenerate package-lock.json after removing @types/jszip from package.json; without this npm ci was still installing the stale 2020 type stubs and TypeScript was resolving against them B: replace dynamic import('jszip') in the zip-bomb test with the same static import already used in production; jszip is CJS-only with no ESM/Jest isolation concern C: document that the _data.uncompressedSize guard fails open if jszip renames the private field (accepted limitation, test would catch it) D: rename 'preserves tabs' test to 'normalizes tab and spacing elements to spaces' since is collapsed to a space, not kept as \t E: fix test.each([ formatting artifact (missing newline after '[') --------- Co-authored-by: Danny Avila --- package-lock.json | 3 + packages/api/package.json | 3 + packages/api/src/files/documents/crud.spec.ts | 68 ++++++++++++++++++ packages/api/src/files/documents/crud.ts | 58 +++++++++++++++ packages/api/src/files/documents/empty.odt | Bin 0 -> 1206 bytes .../src/files/documents/sample-entities.odt | Bin 0 -> 1291 bytes packages/api/src/files/documents/sample.odt | Bin 0 -> 1348 bytes .../data-provider/src/file-config.spec.ts | 3 +- packages/data-provider/src/file-config.ts | 4 +- 9 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/files/documents/empty.odt create mode 100644 packages/api/src/files/documents/sample-entities.odt create mode 100644 packages/api/src/files/documents/sample.odt diff --git a/package-lock.json b/package-lock.json index 2454745a79..a056cc32ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43789,6 +43789,9 @@ "name": "@librechat/api", "version": "1.7.26", "license": "ISC", + "dependencies": { + "jszip": "^3.10.1" + }, "devDependencies": { "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", diff --git a/packages/api/package.json b/packages/api/package.json index 42f1f0e9f0..57675ee371 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -119,5 +119,8 @@ "rate-limit-redis": "^4.2.0", "undici": "^7.24.1", "zod": "^3.22.4" + }, + "dependencies": { + "jszip": "^3.10.1" } } diff --git a/packages/api/src/files/documents/crud.spec.ts b/packages/api/src/files/documents/crud.spec.ts index f8b255dd5e..a1c317279c 100644 --- a/packages/api/src/files/documents/crud.spec.ts +++ b/packages/api/src/files/documents/crud.spec.ts @@ -1,4 +1,6 @@ import path from 'path'; +import * as fs from 'fs'; +import JSZip from 'jszip'; import { parseDocument } from './crud'; describe('Document Parser', () => { @@ -74,6 +76,72 @@ describe('Document Parser', () => { }); }); + test('parseDocument() parses text from odt', async () => { + const file = { + originalname: 'sample.odt', + path: path.join(__dirname, 'sample.odt'), + mimetype: 'application/vnd.oasis.opendocument.text', + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 50, + filename: 'sample.odt', + filepath: 'document_parser', + images: [], + text: 'This is a sample ODT file.\n\nIt has two paragraphs.', + }); + }); + + test('parseDocument() throws for odt with no extractable text', async () => { + const file = { + originalname: 'empty.odt', + path: path.join(__dirname, 'empty.odt'), + mimetype: 'application/vnd.oasis.opendocument.text', + } as Express.Multer.File; + + await expect(parseDocument({ file })).rejects.toThrow('No text found in document'); + }); + + test('parseDocument() throws for odt whose decompressed content exceeds the size limit', async () => { + const zip = new JSZip(); + zip.file('mimetype', 'application/vnd.oasis.opendocument.text', { compression: 'STORE' }); + zip.file('content.xml', 'x'.repeat(51 * 1024 * 1024), { compression: 'DEFLATE' }); + const buf = await zip.generateAsync({ type: 'nodebuffer' }); + + const tmpPath = path.join(__dirname, 'bomb.odt'); + await fs.promises.writeFile(tmpPath, buf); + try { + const file = { + originalname: 'bomb.odt', + path: tmpPath, + mimetype: 'application/vnd.oasis.opendocument.text', + } as Express.Multer.File; + await expect(parseDocument({ file })).rejects.toThrow(/exceeds the 50MB limit/); + } finally { + await fs.promises.unlink(tmpPath); + } + }); + + test('parseDocument() decodes XML entities and normalizes tab and spacing elements to spaces from odt', async () => { + const file = { + originalname: 'sample-entities.odt', + path: path.join(__dirname, 'sample-entities.odt'), + mimetype: 'application/vnd.oasis.opendocument.text', + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 19, + filename: 'sample-entities.odt', + filepath: 'document_parser', + images: [], + text: 'AT&T and A>B\n\nx y z', + }); + }); + test.each([ 'application/msexcel', 'application/x-msexcel', diff --git a/packages/api/src/files/documents/crud.ts b/packages/api/src/files/documents/crud.ts index 61c1956542..e255323f77 100644 --- a/packages/api/src/files/documents/crud.ts +++ b/packages/api/src/files/documents/crud.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import JSZip from 'jszip'; import { megabyte, excelMimeTypes, FileSources } from 'librechat-data-provider'; import type { TextItem } from 'pdfjs-dist/types/src/display/api'; import type { MistralOCRUploadResult } from '~/types'; @@ -6,6 +7,7 @@ import type { MistralOCRUploadResult } from '~/types'; type FileParseFn = (file: Express.Multer.File) => Promise; const DOCUMENT_PARSER_MAX_FILE_SIZE = 15 * megabyte; +const ODT_MAX_DECOMPRESSED_SIZE = 50 * megabyte; /** * Parses an uploaded document and extracts its text content and metadata. @@ -61,6 +63,9 @@ function getParserForMimeType(mimetype: string): FileParseFn | undefined { ) { return excelSheetToText; } + if (mimetype === 'application/vnd.oasis.opendocument.text') { + return odtToText; + } return undefined; } @@ -110,3 +115,56 @@ async function excelSheetToText(file: Express.Multer.File): Promise { return text; } + +/** + * Parses OpenDocument Text (.odt) by extracting the body text from content.xml. + * Uses regex-based XML extraction scoped to : paragraph/heading + * boundaries become newlines, tab and spacing elements are preserved, and the + * five standard XML entities are decoded. Complex elements such as frames, + * text boxes, and annotations are stripped without replacement. + */ +async function odtToText(file: Express.Multer.File): Promise { + const data = await fs.promises.readFile(file.path); + const zip = await JSZip.loadAsync(data); + + let totalUncompressed = 0; + zip.forEach((_, entry) => { + const raw = entry as JSZip.JSZipObject & { _data?: { uncompressedSize?: number } }; + // _data.uncompressedSize is populated from the ZIP central directory at parse time + // by jszip (private internal, jszip@3.x). If the field is absent the guard fails + // open (adds 0); this is an accepted limitation of the approach. + totalUncompressed += raw._data?.uncompressedSize ?? 0; + }); + if (totalUncompressed > ODT_MAX_DECOMPRESSED_SIZE) { + throw new Error( + `ODT file decompressed content (${Math.ceil(totalUncompressed / megabyte)}MB) exceeds the ${ODT_MAX_DECOMPRESSED_SIZE / megabyte}MB limit`, + ); + } + + const contentFile = zip.file('content.xml'); + if (!contentFile) { + throw new Error('ODT file is missing content.xml'); + } + const xml = await contentFile.async('string'); + const bodyMatch = xml.match(/]*>([\s\S]*?)<\/office:body>/); + if (!bodyMatch) { + return ''; + } + return bodyMatch[1] + .replace(/<\/text:p>/g, '\n') + .replace(/<\/text:h>/g, '\n') + .replace(//g, '\n') + .replace(//g, '\t') + .replace(/]*\/>/g, ' ') + .replace(/<[^>]+>/g, '') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/[ \t]+/g, ' ') + .replace(/\n[ \t]+/g, '\n') + .replace(/[ \t]+\n/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); +} diff --git a/packages/api/src/files/documents/empty.odt b/packages/api/src/files/documents/empty.odt new file mode 100644 index 0000000000000000000000000000000000000000..348d78ba0878f468473056e9d49f4db553dbbaa2 GIT binary patch literal 1206 zcmb7Ey-ve05DveB!oUDCkr$>k{4B_7S|I@vLZA+?Ay7zMFiPyGb}RY@z+3PXEQpae z;0a*i9T+$#apDAliX%67=kL3_+@0G!oSJS}YCYVGv-8)@#Wj%Co`W6Jn8;B={3MBJ z;7iJxJ7i&#`+|xlPY4TnFo+40O-XKLx8iK|<7W3_!m`v}0A~SbQXy~SsMUcVdr0~M zJbGxOxsGhY0v=L!<)PD)ePRUD zHEg1B8Y{D?l*eT&Y{t!VGLqjy?S^gZWc`8UF_bEBgd9agxaQ#{4@XKb;mUDl0b3d+ zNg1HIc_=@r$D#jfewd)*soWZ4gCH;>&9fxu`T(yp-mcgw%J`*n4Qq z!ADt>%Yc6;VcY~G9W$PMjdn;+2!}_`R98nXzXlJEO6u$D=Fx;p4Z1y~kkdc?TxX zI8#;0Xj$f2LRD4>av{=5L1iW@vciKIv~aUS9K^JD)7K8nX-u~=jY3v3d<34wquWMFn9yjqH& zRul-znG94CxY7a_9n`suM@!z6t+9sNXmZ3q$MF~;E0(TQ!QoRSzEoAtByl&-inP+} z4z2?m5BTaVWjvklQ(hj<5fv$#?w_l}mmz8PYH|l%8;+u9B(97tQ4LKZe1J*(xz7wq zqu=Ij9Y$Ka9Deh-UtFkXJ9fc3eKnu@uiqS#KJ3fa>mQd;e?1?%Fiu6bVDi7Z!Eyp? zG<{%~o7U3eO+DMypA~Jl7Fo`G=-p+7I^QbK{gSFr5hl2E_o8%e&eDwf`Yc6w!_x%C z({0ry{I>|_csX+1yt?EBT3-haNgLN1P~olZ2j1UrgSOzcVef_^Yk2HC)+v~d$|11> tdY*16Gw#9b$#$>7`YMMlW{IBn)}`5hsjr{e$*tQOHiF+zc<+t$>NlRrc)I`q literal 0 HcmV?d00001 diff --git a/packages/api/src/files/documents/sample.odt b/packages/api/src/files/documents/sample.odt new file mode 100644 index 0000000000000000000000000000000000000000..e6c49d0f29350dc38ab55a72ef21906027b8607d GIT binary patch literal 1348 zcmb7EO>WdM6i&;4U|_)lmPnR2tdj7vK~^#q60KBKg;@+6gpf?)=~zl^EB3UV10VsX zfO}M3_6j`!D;C^==Oj*?Akg3$C(qB{_xw5U+4G|tlWAbvr!V^bhpT7z9)oTBZRjO0 znb{~tl`42f4VNN56?r77<~ov!iCkvuk_i(Tc5dc_VAtQ?J^Hm11h(IWV3`!gBG1br z*@FiL;FAhjYQ zXvs7&8S$kNA5H4D8jqE{E!$#!t~Q&3ZA-4f4ZGcuxAo_EHbuy7m9C8};X`L(P13PH z`BCN@JMQ6DR^tI*tx?SuE3OgzDblo51w(IMEzpt|EK1@QQpe~ShX$>vM&?W+MJswz zQMJ+|bP~7H-IAT7QPmB{$(jwT0t%sPcfdN{U%U(tUmwIn*u~}dAD^LcY=7v(N-Fr0 z>HjJt$05#W3&;((&7;FNsq997CbOHepFM=4uQsUnt&1X5)T~k*U3!zw@Z6(&5v&0vakvKe!_cyVn@FLbE=ah literal 0 HcmV?d00001 diff --git a/packages/data-provider/src/file-config.spec.ts b/packages/data-provider/src/file-config.spec.ts index 0ab9f23a3e..96f48621cc 100644 --- a/packages/data-provider/src/file-config.spec.ts +++ b/packages/data-provider/src/file-config.spec.ts @@ -31,6 +31,7 @@ describe('inferMimeType', () => { expect(inferMimeType('test.py', '')).toBe('text/x-python'); expect(inferMimeType('code.js', '')).toBe('text/javascript'); expect(inferMimeType('photo.heic', '')).toBe('image/heic'); + expect(inferMimeType('Main.java', '')).toBe('text/x-java'); }); it('should return empty string for unknown extension with no browser type', () => { @@ -141,12 +142,12 @@ describe('documentParserMimeTypes', () => { 'application/x-msexcel', 'application/x-ms-excel', 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.text', ])('matches natively parseable type: %s', (mimeType) => { expect(check(mimeType)).toBe(true); }); it.each([ - 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.graphics', 'text/plain', diff --git a/packages/data-provider/src/file-config.ts b/packages/data-provider/src/file-config.ts index 67b4197958..32a1a28cc9 100644 --- a/packages/data-provider/src/file-config.ts +++ b/packages/data-provider/src/file-config.ts @@ -202,12 +202,13 @@ export const defaultOCRMimeTypes = [ /^application\/vnd\.oasis\.opendocument\.(text|spreadsheet|presentation|graphics)$/, ]; -/** MIME types handled by the built-in document parser (pdf, docx, excel variants, ods) */ +/** MIME types handled by the built-in document parser (pdf, docx, excel variants, ods/odt) */ export const documentParserMimeTypes = [ excelMimeTypes, /^application\/pdf$/, /^application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document$/, /^application\/vnd\.oasis\.opendocument\.spreadsheet$/, + /^application\/vnd\.oasis\.opendocument\.text$/, ]; export const defaultTextMimeTypes = [/^[\w.-]+\/[\w.-]+$/]; @@ -242,6 +243,7 @@ export const codeTypeMapping: { [key: string]: string } = { py: 'text/x-python', // .py - Python source rb: 'text/x-ruby', // .rb - Ruby source tex: 'text/x-tex', // .tex - LaTeX source + java: 'text/x-java', // .java - Java source js: 'text/javascript', // .js - JavaScript source sh: 'application/x-sh', // .sh - Shell script ts: 'application/typescript', // .ts - TypeScript source From 39f5f83a8a2a3cbc6e86ccdd50dd027fa2d5e156 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 16:16:57 -0400 Subject: [PATCH 070/111] =?UTF-8?q?=F0=9F=94=8C=20fix:=20Isolate=20Code-Se?= =?UTF-8?q?rver=20HTTP=20Agents=20to=20Prevent=20Socket=20Pool=20Contamina?= =?UTF-8?q?tion=20(#12311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 fix: Isolate HTTP agents for code-server axios requests Prevents socket hang up after 5s on Node 19+ when code executor has file attachments. follow-redirects (axios dep) leaks `socket.destroy` as a timeout listener on TCP sockets; with Node 19+ defaulting to keepAlive: true, tainted sockets re-enter the global pool and destroy active node-fetch requests in CodeExecutor after the idle timeout. Uses dedicated http/https agents with keepAlive: false for all axios calls targeting CODE_BASEURL in crud.js and process.js. Closes #12298 * ♻️ refactor: Extract code-server HTTP agents to shared module - Move duplicated agent construction from crud.js and process.js into a shared agents.js module to eliminate DRY violation - Switch process.js from raw `require('axios')` to `createAxiosInstance()` for proxy configuration parity with crud.js - Fix import ordering in process.js (agent constants no longer split imports) - Add 120s timeout to uploadCodeEnvFile (was the only code-server call without a timeout) * ✅ test: Add regression tests for code-server socket isolation - Add crud.spec.js covering getCodeOutputDownloadStream and uploadCodeEnvFile (agent options, timeout, URL, error handling) - Add socket pool isolation tests to process.spec.js asserting keepAlive:false agents are forwarded to axios - Update process.spec.js mocks for createAxiosInstance() migration * ♻️ refactor: Move code-server agents to packages/api Relocate agents.js from api/server/services/Files/Code/ to packages/api/src/utils/code.ts per workspace conventions. Consumers now import codeServerHttpAgent/codeServerHttpsAgent from @librechat/api. --- .../Code/__tests__/process-traversal.spec.js | 28 ++-- api/server/services/Files/Code/crud.js | 12 +- api/server/services/Files/Code/crud.spec.js | 149 ++++++++++++++++++ api/server/services/Files/Code/process.js | 17 +- .../services/Files/Code/process.spec.js | 100 ++++++++---- packages/api/src/utils/code.ts | 11 ++ packages/api/src/utils/index.ts | 1 + 7 files changed, 273 insertions(+), 45 deletions(-) create mode 100644 api/server/services/Files/Code/crud.spec.js create mode 100644 packages/api/src/utils/code.ts diff --git a/api/server/services/Files/Code/__tests__/process-traversal.spec.js b/api/server/services/Files/Code/__tests__/process-traversal.spec.js index 2db366d06b..0b8548445d 100644 --- a/api/server/services/Files/Code/__tests__/process-traversal.spec.js +++ b/api/server/services/Files/Code/__tests__/process-traversal.spec.js @@ -10,11 +10,23 @@ jest.mock('@librechat/agents', () => ({ const mockSanitizeFilename = jest.fn(); -jest.mock('@librechat/api', () => ({ - logAxiosError: jest.fn(), - getBasePath: jest.fn(() => ''), - sanitizeFilename: mockSanitizeFilename, -})); +const mockAxios = jest.fn().mockResolvedValue({ + data: Buffer.from('file-content'), +}); +mockAxios.post = jest.fn(); + +jest.mock('@librechat/api', () => { + const http = require('http'); + const https = require('https'); + return { + logAxiosError: jest.fn(), + getBasePath: jest.fn(() => ''), + sanitizeFilename: mockSanitizeFilename, + createAxiosInstance: jest.fn(() => mockAxios), + codeServerHttpAgent: new http.Agent({ keepAlive: false }), + codeServerHttpsAgent: new https.Agent({ keepAlive: false }), + }; +}); jest.mock('librechat-data-provider', () => ({ ...jest.requireActual('librechat-data-provider'), @@ -53,12 +65,6 @@ jest.mock('~/server/utils', () => ({ determineFileType: jest.fn().mockResolvedValue({ mime: 'text/csv' }), })); -jest.mock('axios', () => - jest.fn().mockResolvedValue({ - data: Buffer.from('file-content'), - }), -); - const { createFile } = require('~/models'); const { processCodeOutput } = require('../process'); diff --git a/api/server/services/Files/Code/crud.js b/api/server/services/Files/Code/crud.js index 4781219fcf..945aec787b 100644 --- a/api/server/services/Files/Code/crud.js +++ b/api/server/services/Files/Code/crud.js @@ -1,6 +1,11 @@ const FormData = require('form-data'); const { getCodeBaseURL } = require('@librechat/agents'); -const { createAxiosInstance, logAxiosError } = require('@librechat/api'); +const { + logAxiosError, + createAxiosInstance, + codeServerHttpAgent, + codeServerHttpsAgent, +} = require('@librechat/api'); const axios = createAxiosInstance(); @@ -25,6 +30,8 @@ async function getCodeOutputDownloadStream(fileIdentifier, apiKey) { 'User-Agent': 'LibreChat/1.0', 'X-API-Key': apiKey, }, + httpAgent: codeServerHttpAgent, + httpsAgent: codeServerHttpsAgent, timeout: 15000, }; @@ -69,6 +76,9 @@ async function uploadCodeEnvFile({ req, stream, filename, apiKey, entity_id = '' 'User-Id': req.user.id, 'X-API-Key': apiKey, }, + httpAgent: codeServerHttpAgent, + httpsAgent: codeServerHttpsAgent, + timeout: 120000, maxContentLength: MAX_FILE_SIZE, maxBodyLength: MAX_FILE_SIZE, }; diff --git a/api/server/services/Files/Code/crud.spec.js b/api/server/services/Files/Code/crud.spec.js new file mode 100644 index 0000000000..261f0f052b --- /dev/null +++ b/api/server/services/Files/Code/crud.spec.js @@ -0,0 +1,149 @@ +const http = require('http'); +const https = require('https'); +const { Readable } = require('stream'); + +const mockAxios = jest.fn(); +mockAxios.post = jest.fn(); + +jest.mock('@librechat/agents', () => ({ + getCodeBaseURL: jest.fn(() => 'https://code-api.example.com'), +})); + +jest.mock('@librechat/api', () => { + const http = require('http'); + const https = require('https'); + return { + logAxiosError: jest.fn(({ message }) => message), + createAxiosInstance: jest.fn(() => mockAxios), + codeServerHttpAgent: new http.Agent({ keepAlive: false }), + codeServerHttpsAgent: new https.Agent({ keepAlive: false }), + }; +}); + +const { codeServerHttpAgent, codeServerHttpsAgent } = require('@librechat/api'); +const { getCodeOutputDownloadStream, uploadCodeEnvFile } = require('./crud'); + +describe('Code CRUD', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getCodeOutputDownloadStream', () => { + it('should pass dedicated keepAlive:false agents to axios', async () => { + const mockResponse = { data: Readable.from(['chunk']) }; + mockAxios.mockResolvedValue(mockResponse); + + await getCodeOutputDownloadStream('session-1/file-1', 'test-key'); + + const callConfig = mockAxios.mock.calls[0][0]; + expect(callConfig.httpAgent).toBe(codeServerHttpAgent); + expect(callConfig.httpsAgent).toBe(codeServerHttpsAgent); + expect(callConfig.httpAgent).toBeInstanceOf(http.Agent); + expect(callConfig.httpsAgent).toBeInstanceOf(https.Agent); + expect(callConfig.httpAgent.keepAlive).toBe(false); + expect(callConfig.httpsAgent.keepAlive).toBe(false); + }); + + it('should request stream response from the correct URL', async () => { + mockAxios.mockResolvedValue({ data: Readable.from(['chunk']) }); + + await getCodeOutputDownloadStream('session-1/file-1', 'test-key'); + + const callConfig = mockAxios.mock.calls[0][0]; + expect(callConfig.url).toBe('https://code-api.example.com/download/session-1/file-1'); + expect(callConfig.responseType).toBe('stream'); + expect(callConfig.timeout).toBe(15000); + expect(callConfig.headers['X-API-Key']).toBe('test-key'); + }); + + it('should throw on network error', async () => { + mockAxios.mockRejectedValue(new Error('ECONNREFUSED')); + + await expect(getCodeOutputDownloadStream('s/f', 'key')).rejects.toThrow(); + }); + }); + + describe('uploadCodeEnvFile', () => { + const baseUploadParams = { + req: { user: { id: 'user-123' } }, + stream: Readable.from(['file-content']), + filename: 'data.csv', + apiKey: 'test-key', + }; + + it('should pass dedicated keepAlive:false agents to axios', async () => { + mockAxios.post.mockResolvedValue({ + data: { + message: 'success', + session_id: 'sess-1', + files: [{ fileId: 'fid-1', filename: 'data.csv' }], + }, + }); + + await uploadCodeEnvFile(baseUploadParams); + + const callConfig = mockAxios.post.mock.calls[0][2]; + expect(callConfig.httpAgent).toBe(codeServerHttpAgent); + expect(callConfig.httpsAgent).toBe(codeServerHttpsAgent); + expect(callConfig.httpAgent).toBeInstanceOf(http.Agent); + expect(callConfig.httpsAgent).toBeInstanceOf(https.Agent); + expect(callConfig.httpAgent.keepAlive).toBe(false); + expect(callConfig.httpsAgent.keepAlive).toBe(false); + }); + + it('should set a timeout on upload requests', async () => { + mockAxios.post.mockResolvedValue({ + data: { + message: 'success', + session_id: 'sess-1', + files: [{ fileId: 'fid-1', filename: 'data.csv' }], + }, + }); + + await uploadCodeEnvFile(baseUploadParams); + + const callConfig = mockAxios.post.mock.calls[0][2]; + expect(callConfig.timeout).toBe(120000); + }); + + it('should return fileIdentifier on success', async () => { + mockAxios.post.mockResolvedValue({ + data: { + message: 'success', + session_id: 'sess-1', + files: [{ fileId: 'fid-1', filename: 'data.csv' }], + }, + }); + + const result = await uploadCodeEnvFile(baseUploadParams); + expect(result).toBe('sess-1/fid-1'); + }); + + it('should append entity_id query param when provided', async () => { + mockAxios.post.mockResolvedValue({ + data: { + message: 'success', + session_id: 'sess-1', + files: [{ fileId: 'fid-1', filename: 'data.csv' }], + }, + }); + + const result = await uploadCodeEnvFile({ ...baseUploadParams, entity_id: 'agent-42' }); + expect(result).toBe('sess-1/fid-1?entity_id=agent-42'); + }); + + it('should throw when server returns non-success message', async () => { + mockAxios.post.mockResolvedValue({ + data: { message: 'quota_exceeded', session_id: 's', files: [] }, + }); + + await expect(uploadCodeEnvFile(baseUploadParams)).rejects.toThrow('quota_exceeded'); + }); + + it('should throw on network error', async () => { + mockAxios.post.mockRejectedValue(new Error('ECONNREFUSED')); + + await expect(uploadCodeEnvFile(baseUploadParams)).rejects.toThrow(); + }); + }); +}); diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js index e878b00255..7cdebeb202 100644 --- a/api/server/services/Files/Code/process.js +++ b/api/server/services/Files/Code/process.js @@ -1,9 +1,15 @@ const path = require('path'); const { v4 } = require('uuid'); -const axios = require('axios'); const { logger } = require('@librechat/data-schemas'); const { getCodeBaseURL } = require('@librechat/agents'); -const { logAxiosError, getBasePath, sanitizeFilename } = require('@librechat/api'); +const { + getBasePath, + logAxiosError, + sanitizeFilename, + createAxiosInstance, + codeServerHttpAgent, + codeServerHttpsAgent, +} = require('@librechat/api'); const { Tools, megabyte, @@ -23,6 +29,8 @@ const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { convertImage } = require('~/server/services/Files/images/convert'); const { determineFileType } = require('~/server/utils'); +const axios = createAxiosInstance(); + /** * Creates a fallback download URL response when file cannot be processed locally. * Used when: file exceeds size limit, storage strategy unavailable, or download error occurs. @@ -102,6 +110,8 @@ const processCodeOutput = async ({ 'User-Agent': 'LibreChat/1.0', 'X-API-Key': apiKey, }, + httpAgent: codeServerHttpAgent, + httpsAgent: codeServerHttpsAgent, timeout: 15000, }); @@ -300,6 +310,8 @@ async function getSessionInfo(fileIdentifier, apiKey) { 'User-Agent': 'LibreChat/1.0', 'X-API-Key': apiKey, }, + httpAgent: codeServerHttpAgent, + httpsAgent: codeServerHttpsAgent, timeout: 5000, }); @@ -448,5 +460,6 @@ const primeFiles = async (options, apiKey) => { module.exports = { primeFiles, + getSessionInfo, processCodeOutput, }; diff --git a/api/server/services/Files/Code/process.spec.js b/api/server/services/Files/Code/process.spec.js index b89a6c6307..a805ee2bcc 100644 --- a/api/server/services/Files/Code/process.spec.js +++ b/api/server/services/Files/Code/process.spec.js @@ -36,11 +36,24 @@ jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid-1234'), })); -// Mock axios -jest.mock('axios'); -const axios = require('axios'); +// Mock axios — process.js now uses createAxiosInstance() from @librechat/api +const mockAxios = jest.fn(); +mockAxios.post = jest.fn(); +mockAxios.isAxiosError = jest.fn(() => false); + +jest.mock('@librechat/api', () => { + const http = require('http'); + const https = require('https'); + return { + logAxiosError: jest.fn(), + getBasePath: jest.fn(() => ''), + sanitizeFilename: jest.fn((name) => name), + createAxiosInstance: jest.fn(() => mockAxios), + codeServerHttpAgent: new http.Agent({ keepAlive: false }), + codeServerHttpsAgent: new https.Agent({ keepAlive: false }), + }; +}); -// Mock logger jest.mock('@librechat/data-schemas', () => ({ logger: { warn: jest.fn(), @@ -49,18 +62,10 @@ jest.mock('@librechat/data-schemas', () => ({ }, })); -// Mock getCodeBaseURL jest.mock('@librechat/agents', () => ({ getCodeBaseURL: jest.fn(() => 'https://code-api.example.com'), })); -// Mock logAxiosError and getBasePath -jest.mock('@librechat/api', () => ({ - logAxiosError: jest.fn(), - getBasePath: jest.fn(() => ''), - sanitizeFilename: jest.fn((name) => name), -})); - // Mock models const mockClaimCodeFile = jest.fn(); jest.mock('~/models', () => ({ @@ -90,14 +95,16 @@ jest.mock('~/server/utils', () => ({ determineFileType: jest.fn(), })); +const http = require('http'); +const https = require('https'); const { createFile, getFiles } = require('~/models'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { convertImage } = require('~/server/services/Files/images/convert'); const { determineFileType } = require('~/server/utils'); const { logger } = require('@librechat/data-schemas'); +const { codeServerHttpAgent, codeServerHttpsAgent } = require('@librechat/api'); -// Import after mocks -const { processCodeOutput } = require('./process'); +const { processCodeOutput, getSessionInfo } = require('./process'); describe('Code Process', () => { const mockReq = { @@ -145,7 +152,7 @@ describe('Code Process', () => { }); const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -168,7 +175,7 @@ describe('Code Process', () => { }); const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -182,7 +189,7 @@ describe('Code Process', () => { it('should process image files using convertImage', async () => { const imageParams = { ...baseParams, name: 'chart.png' }; const imageBuffer = Buffer.alloc(500); - axios.mockResolvedValue({ data: imageBuffer }); + mockAxios.mockResolvedValue({ data: imageBuffer }); const convertedFile = { filepath: '/uploads/converted-image.webp', @@ -212,7 +219,7 @@ describe('Code Process', () => { }); const imageBuffer = Buffer.alloc(500); - axios.mockResolvedValue({ data: imageBuffer }); + mockAxios.mockResolvedValue({ data: imageBuffer }); convertImage.mockResolvedValue({ filepath: '/images/user-123/existing-img-id.webp' }); const result = await processCodeOutput(imageParams); @@ -235,7 +242,7 @@ describe('Code Process', () => { describe('non-image file processing', () => { it('should process non-image files using saveBuffer', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const mockSaveBuffer = jest.fn().mockResolvedValue('/uploads/saved-file.txt'); getStrategyFunctions.mockReturnValue({ saveBuffer: mockSaveBuffer }); @@ -256,7 +263,7 @@ describe('Code Process', () => { it('should detect MIME type from buffer', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); determineFileType.mockResolvedValue({ mime: 'application/pdf' }); const result = await processCodeOutput({ ...baseParams, name: 'document.pdf' }); @@ -267,7 +274,7 @@ describe('Code Process', () => { it('should fallback to application/octet-stream for unknown types', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); determineFileType.mockResolvedValue(null); const result = await processCodeOutput({ ...baseParams, name: 'unknown.xyz' }); @@ -282,7 +289,7 @@ describe('Code Process', () => { fileSizeLimitConfig.value = 1000; // 1KB limit const largeBuffer = Buffer.alloc(5000); // 5KB - exceeds 1KB limit - axios.mockResolvedValue({ data: largeBuffer }); + mockAxios.mockResolvedValue({ data: largeBuffer }); const result = await processCodeOutput(baseParams); @@ -300,7 +307,7 @@ describe('Code Process', () => { describe('fallback behavior', () => { it('should fallback to download URL when saveBuffer is not available', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); getStrategyFunctions.mockReturnValue({ saveBuffer: null }); const result = await processCodeOutput(baseParams); @@ -313,7 +320,7 @@ describe('Code Process', () => { }); it('should fallback to download URL on axios error', async () => { - axios.mockRejectedValue(new Error('Network error')); + mockAxios.mockRejectedValue(new Error('Network error')); const result = await processCodeOutput(baseParams); @@ -327,7 +334,7 @@ describe('Code Process', () => { describe('usage counter increment', () => { it('should set usage to 1 for new files', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -341,7 +348,7 @@ describe('Code Process', () => { createdAt: '2024-01-01', }); const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -354,7 +361,7 @@ describe('Code Process', () => { createdAt: '2024-01-01', }); const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -365,7 +372,7 @@ describe('Code Process', () => { describe('metadata and file properties', () => { it('should include fileIdentifier in metadata', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -376,7 +383,7 @@ describe('Code Process', () => { it('should set correct context for code-generated files', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -385,7 +392,7 @@ describe('Code Process', () => { it('should include toolCallId and messageId in result', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); const result = await processCodeOutput(baseParams); @@ -395,7 +402,7 @@ describe('Code Process', () => { it('should call createFile with upsert enabled', async () => { const smallBuffer = Buffer.alloc(100); - axios.mockResolvedValue({ data: smallBuffer }); + mockAxios.mockResolvedValue({ data: smallBuffer }); await processCodeOutput(baseParams); @@ -408,5 +415,36 @@ describe('Code Process', () => { ); }); }); + + describe('socket pool isolation', () => { + it('should pass dedicated keepAlive:false agents to axios for processCodeOutput', async () => { + const smallBuffer = Buffer.alloc(100); + mockAxios.mockResolvedValue({ data: smallBuffer }); + + await processCodeOutput(baseParams); + + const callConfig = mockAxios.mock.calls[0][0]; + expect(callConfig.httpAgent).toBe(codeServerHttpAgent); + expect(callConfig.httpsAgent).toBe(codeServerHttpsAgent); + expect(callConfig.httpAgent).toBeInstanceOf(http.Agent); + expect(callConfig.httpsAgent).toBeInstanceOf(https.Agent); + expect(callConfig.httpAgent.keepAlive).toBe(false); + expect(callConfig.httpsAgent.keepAlive).toBe(false); + }); + + it('should pass dedicated keepAlive:false agents to axios for getSessionInfo', async () => { + mockAxios.mockResolvedValue({ + data: [{ name: 'sess/fid', lastModified: new Date().toISOString() }], + }); + + await getSessionInfo('sess/fid', 'api-key'); + + const callConfig = mockAxios.mock.calls[0][0]; + expect(callConfig.httpAgent).toBe(codeServerHttpAgent); + expect(callConfig.httpsAgent).toBe(codeServerHttpsAgent); + expect(callConfig.httpAgent.keepAlive).toBe(false); + expect(callConfig.httpsAgent.keepAlive).toBe(false); + }); + }); }); }); diff --git a/packages/api/src/utils/code.ts b/packages/api/src/utils/code.ts new file mode 100644 index 0000000000..9ac814a52b --- /dev/null +++ b/packages/api/src/utils/code.ts @@ -0,0 +1,11 @@ +import http from 'http'; +import https from 'https'; + +/** + * Dedicated agents for code-server requests, preventing socket pool contamination. + * follow-redirects (used by axios) leaks `socket.destroy` as a timeout listener; + * on Node 19+ (keepAlive: true by default), tainted sockets re-enter the global pool + * and kill unrelated requests (e.g., node-fetch in CodeExecutor) after the idle timeout. + */ +export const codeServerHttpAgent = new http.Agent({ keepAlive: false }); +export const codeServerHttpsAgent = new https.Agent({ keepAlive: false }); diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 5b9315d8c7..a1412e21f2 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './axios'; export * from './azure'; +export * from './code'; export * from './common'; export * from './content'; export * from './email'; From 11ab5f6ee5e5ae955a65bedf00f68a9c2a2d2ecc Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 16:42:57 -0400 Subject: [PATCH 071/111] =?UTF-8?q?=F0=9F=9B=82=20fix:=20Reject=20OpenID?= =?UTF-8?q?=20Email=20Fallback=20When=20Stored=20`openidId`=20Mismatches?= =?UTF-8?q?=20Token=20Sub=20(#12312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔐 fix: Reject OpenID email fallback when stored openidId mismatches token sub When `findOpenIDUser` falls back to email lookup after the primary `openidId`/`idOnTheSource` query fails, it now rejects any user whose stored `openidId` differs from the incoming JWT subject claim. This closes an account-takeover vector where a valid IdP JWT containing a victim's email but a different `sub` could authenticate as the victim when OPENID_REUSE_TOKENS is enabled. The migration path (user has no `openidId` yet) is unaffected. * test: Validate openidId mismatch guard in email fallback path Update `findOpenIDUser` unit tests to assert that email-based lookups returning a user with a different `openidId` are rejected with AUTH_FAILED. Add matching integration test in `openIdJwtStrategy.spec` exercising the full verify callback with the real `findOpenIDUser`. * 🔐 fix: Remove redundant `openidId` truthiness check from mismatch guard The `&& openidId` middle term in the guard condition caused it to be bypassed when the incoming token `sub` was empty or undefined. Since the JS callers can pass `payload?.sub` (which may be undefined), this created a path where the guard never fired and the email fallback returned the victim's account. Removing the term ensures the guard rejects whenever the stored openidId differs from the incoming value, regardless of whether the incoming value is falsy. * test: Cover falsy openidId bypass and openidStrategy mismatch rejection Add regression test for the guard bypass when `openidId` is an empty string and the email lookup finds a user with a stored openidId. Add integration test in openidStrategy.spec.js exercising the mismatch rejection through the full processOpenIDAuth callback, ensuring both OIDC paths (JWT reuse and standard callback) are covered. Restore intent-documenting comment on the no-provider fixture. --- api/strategies/openIdJwtStrategy.spec.js | 26 +++++++ api/strategies/openidStrategy.spec.js | 27 +++++++ packages/api/src/auth/openid.spec.ts | 95 ++++++++++++++++++------ packages/api/src/auth/openid.ts | 8 +- 4 files changed, 133 insertions(+), 23 deletions(-) diff --git a/api/strategies/openIdJwtStrategy.spec.js b/api/strategies/openIdJwtStrategy.spec.js index 79af848046..fd710f1ebd 100644 --- a/api/strategies/openIdJwtStrategy.spec.js +++ b/api/strategies/openIdJwtStrategy.spec.js @@ -271,6 +271,32 @@ describe('openIdJwtStrategy – OPENID_EMAIL_CLAIM', () => { expect(user).toBe(false); }); + it('should reject login when email fallback finds user with mismatched openidId', async () => { + const emailMatchWithDifferentSub = { + _id: 'user-id-2', + provider: 'openid', + openidId: 'different-sub', + email: payload.email, + role: SystemRoles.USER, + }; + + findUser.mockImplementation(async (query) => { + if (query.$or) { + return null; + } + if (query.email === payload.email) { + return emailMatchWithDifferentSub; + } + return null; + }); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + const { user, info } = await invokeVerify(req, payload); + + expect(user).toBe(false); + expect(info).toEqual({ message: 'auth_failed' }); + }); + it('should trim whitespace from OPENID_EMAIL_CLAIM', async () => { process.env.OPENID_EMAIL_CLAIM = ' upn '; findUser.mockResolvedValue(null); diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js index 16fa548a59..4436fab672 100644 --- a/api/strategies/openidStrategy.spec.js +++ b/api/strategies/openidStrategy.spec.js @@ -356,6 +356,33 @@ describe('setupOpenId', () => { expect(updateUser).not.toHaveBeenCalled(); }); + it('should block login when email fallback finds user with mismatched openidId', async () => { + const existingUser = { + _id: 'existingUserId', + provider: 'openid', + openidId: 'different-sub-claim', + email: tokenset.claims().email, + username: 'existinguser', + name: 'Existing User', + }; + findUser.mockImplementation(async (query) => { + if (query.$or) { + return null; + } + if (query.email === tokenset.claims().email) { + return existingUser; + } + return null; + }); + + const result = await validate(tokenset); + + expect(result.user).toBe(false); + expect(result.details.message).toBe(ErrorTypes.AUTH_FAILED); + expect(createUser).not.toHaveBeenCalled(); + expect(updateUser).not.toHaveBeenCalled(); + }); + it('should enforce the required role and reject login if missing', async () => { // Arrange – simulate a token without the required role. jwtDecode.mockReturnValue({ diff --git a/packages/api/src/auth/openid.spec.ts b/packages/api/src/auth/openid.spec.ts index 7349508ce1..0761a24e85 100644 --- a/packages/api/src/auth/openid.spec.ts +++ b/packages/api/src/auth/openid.spec.ts @@ -107,18 +107,18 @@ describe('findOpenIDUser', () => { }); describe('Email-based searches', () => { - it('should find user by email when primary conditions fail', async () => { + it('should find user by email when primary conditions fail and openidId matches', async () => { const mockUser: IUser = { _id: 'user123', provider: 'openid', - openidId: 'openid_456', + openidId: 'openid_123', email: 'user@example.com', username: 'testuser', } as IUser; mockFindUser - .mockResolvedValueOnce(null) // Primary condition fails - .mockResolvedValueOnce(mockUser); // Email search succeeds + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockUser); const result = await findOpenIDUser({ openidId: 'openid_123', @@ -202,7 +202,7 @@ describe('findOpenIDUser', () => { }); }); - it('should allow login when user has openid provider', async () => { + it('should reject email fallback when existing openidId does not match token sub', async () => { const mockUser: IUser = { _id: 'user123', provider: 'openid', @@ -212,8 +212,34 @@ describe('findOpenIDUser', () => { } as IUser; mockFindUser - .mockResolvedValueOnce(null) // Primary condition fails - .mockResolvedValueOnce(mockUser); // Email search finds user with openid provider + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockUser); + + const result = await findOpenIDUser({ + openidId: 'openid_123', + findUser: mockFindUser, + email: 'user@example.com', + }); + + expect(result).toEqual({ + user: null, + error: ErrorTypes.AUTH_FAILED, + migration: false, + }); + }); + + it('should allow email fallback when existing openidId matches token sub', async () => { + const mockUser: IUser = { + _id: 'user123', + provider: 'openid', + openidId: 'openid_123', + email: 'user@example.com', + username: 'testuser', + } as IUser; + + mockFindUser + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockUser); const result = await findOpenIDUser({ openidId: 'openid_123', @@ -259,7 +285,7 @@ describe('findOpenIDUser', () => { }); }); - it('should not migrate user who already has openidId', async () => { + it('should reject when user already has a different openidId', async () => { const mockUser: IUser = { _id: 'user123', provider: 'openid', @@ -269,8 +295,8 @@ describe('findOpenIDUser', () => { } as IUser; mockFindUser - .mockResolvedValueOnce(null) // Primary condition fails - .mockResolvedValueOnce(mockUser); // Email search finds user with existing openidId + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockUser); const result = await findOpenIDUser({ openidId: 'openid_123', @@ -279,24 +305,24 @@ describe('findOpenIDUser', () => { }); expect(result).toEqual({ - user: mockUser, - error: null, + user: null, + error: ErrorTypes.AUTH_FAILED, migration: false, }); }); - it('should handle user with no provider but existing openidId', async () => { + it('should reject when user has no provider but a different openidId', async () => { const mockUser: IUser = { _id: 'user123', openidId: 'existing_openid', email: 'user@example.com', username: 'testuser', - // No provider field + // No provider field — tests a different branch than openid-provider mismatch } as IUser; mockFindUser - .mockResolvedValueOnce(null) // Primary condition fails - .mockResolvedValueOnce(mockUser); // Email search finds user + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockUser); const result = await findOpenIDUser({ openidId: 'openid_123', @@ -305,8 +331,8 @@ describe('findOpenIDUser', () => { }); expect(result).toEqual({ - user: mockUser, - error: null, + user: null, + error: ErrorTypes.AUTH_FAILED, migration: false, }); }); @@ -398,14 +424,14 @@ describe('findOpenIDUser', () => { const mockUser: IUser = { _id: 'user123', provider: 'openid', - openidId: 'openid_456', + openidId: 'openid_123', email: 'user@example.com', username: 'testuser', } as IUser; mockFindUser - .mockResolvedValueOnce(null) // Primary condition fails - .mockResolvedValueOnce(mockUser); // Email search succeeds + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockUser); const result = await findOpenIDUser({ openidId: 'openid_123', @@ -413,7 +439,6 @@ describe('findOpenIDUser', () => { email: 'User@Example.COM', }); - /** Email is passed as-is; findUser implementation handles normalization */ expect(mockFindUser).toHaveBeenNthCalledWith(2, { email: 'User@Example.COM' }); expect(result).toEqual({ user: mockUser, @@ -432,5 +457,31 @@ describe('findOpenIDUser', () => { }), ).rejects.toThrow('Database error'); }); + + it('should reject email fallback when openidId is empty and user has a stored openidId', async () => { + const mockUser: IUser = { + _id: 'user123', + provider: 'openid', + openidId: 'existing-real-id', + email: 'user@example.com', + username: 'testuser', + } as IUser; + + mockFindUser.mockResolvedValueOnce(mockUser); + + const result = await findOpenIDUser({ + openidId: '', + findUser: mockFindUser, + email: 'user@example.com', + }); + + expect(mockFindUser).toHaveBeenCalledTimes(1); + expect(mockFindUser).toHaveBeenCalledWith({ email: 'user@example.com' }); + expect(result).toEqual({ + user: null, + error: ErrorTypes.AUTH_FAILED, + migration: false, + }); + }); }); }); diff --git a/packages/api/src/auth/openid.ts b/packages/api/src/auth/openid.ts index a7079ccd16..12ff48b2a9 100644 --- a/packages/api/src/auth/openid.ts +++ b/packages/api/src/auth/openid.ts @@ -47,7 +47,13 @@ export async function findOpenIDUser({ return { user: null, error: ErrorTypes.AUTH_FAILED, migration: false }; } - // If user found by email but doesn't have openidId, prepare for migration + if (user?.openidId && user.openidId !== openidId) { + logger.warn( + `[${strategyName}] Rejected email fallback for ${user.email}: stored openidId does not match token sub`, + ); + return { user: null, error: ErrorTypes.AUTH_FAILED, migration: false }; + } + if (user && !user.openidId) { logger.info( `[${strategyName}] Preparing user ${user.email} for migration to OpenID with sub: ${openidId}`, From 3abad53c16d9b2ada2a5e7c8dfa3cd8fac0b74b7 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 16:44:38 -0400 Subject: [PATCH 072/111] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20`@dicebe?= =?UTF-8?q?ar`=20dependencies=20to=20v9.4.1=20(#12315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump @dicebear/collection and @dicebear/core to version 9.4.1 across multiple package files for consistency and improved functionality. - Update related dependencies in the client and packages/client directories to ensure compatibility with the new versions. --- client/package.json | 4 +- package-lock.json | 307 ++++++++++++++++++----------------- packages/client/package.json | 4 +- 3 files changed, 164 insertions(+), 151 deletions(-) diff --git a/client/package.json b/client/package.json index a3ff5529e5..f42834c1c2 100644 --- a/client/package.json +++ b/client/package.json @@ -32,8 +32,8 @@ "@ariakit/react": "^0.4.15", "@ariakit/react-core": "^0.4.17", "@codesandbox/sandpack-react": "^2.19.10", - "@dicebear/collection": "^9.2.2", - "@dicebear/core": "^9.2.2", + "@dicebear/collection": "^9.4.1", + "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", diff --git a/package-lock.json b/package-lock.json index a056cc32ef..0b0c0e4888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -435,8 +435,8 @@ "@ariakit/react": "^0.4.15", "@ariakit/react-core": "^0.4.17", "@codesandbox/sandpack-react": "^2.19.10", - "@dicebear/collection": "^9.2.2", - "@dicebear/core": "^9.2.2", + "@dicebear/collection": "^9.4.1", + "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", @@ -8538,10 +8538,10 @@ } }, "node_modules/@dicebear/adventurer": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.2.4.tgz", - "integrity": "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.4.1.tgz", + "integrity": "sha512-AVEbLK45t6kLnSqcL3AB3Mm3kHhlqpLL6Pa4i9+Jis2O6iwmBZ+x/qmFqV2jQuIxxe55oMRzJLuYGdKWLM8mgg==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8550,10 +8550,10 @@ } }, "node_modules/@dicebear/adventurer-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.2.4.tgz", - "integrity": "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.4.1.tgz", + "integrity": "sha512-5GLdGGpTfwb8Yw5V/nMUim/Re5SgMpDLBpGN/hvlIgobkQt9CnIxTYtSTXmWg+EO8WEAGgMMsj36ts4ELI/CRA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8562,10 +8562,10 @@ } }, "node_modules/@dicebear/avataaars": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.2.4.tgz", - "integrity": "sha512-QKNBtA/1QGEzR+JjS4XQyrFHYGbzdOp0oa6gjhGhUDrMegDFS8uyjdRfDQsFTebVkyLWjgBQKZEiDqKqHptB6A==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.4.1.tgz", + "integrity": "sha512-qLloK9a7DZoASkjyYWNQpG7TwyIBORJvd5r/h8P0ZRAXvbHRrbpWzM3DT8XEvMU57Dav2i7VC/WdnJwV+72Wng==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -8574,10 +8574,10 @@ } }, "node_modules/@dicebear/avataaars-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.2.4.tgz", - "integrity": "sha512-HtBvA7elRv50QTOOsBdtYB1GVimCpGEDlDgWsu1snL5Z3d1+3dIESoXQd3mXVvKTVT8Z9ciA4TEaF09WfxDjAA==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.4.1.tgz", + "integrity": "sha512-z5jFq361OKqjXBJnAm3U20+Wducrp3f+Lr2DFDEYFMQXtJ5DiklGcggof3cinueqG8zKFyhcA5oUq06FPUU47A==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -8586,10 +8586,10 @@ } }, "node_modules/@dicebear/big-ears": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.2.4.tgz", - "integrity": "sha512-U33tbh7Io6wG6ViUMN5fkWPER7hPKMaPPaYgafaYQlCT4E7QPKF2u8X1XGag3jCKm0uf4SLXfuZ8v+YONcHmNQ==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.4.1.tgz", + "integrity": "sha512-30P4Q3n0pCgfFwVgiFTm+dQiJUmF+j8I71nQM+dUIGynrzkGq1vGSdhyTWfr8g/X7wPgsHhW1GEro71o0d1wvA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8598,10 +8598,10 @@ } }, "node_modules/@dicebear/big-ears-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.2.4.tgz", - "integrity": "sha512-pPjYu80zMFl43A9sa5+tAKPkhp4n9nd7eN878IOrA1HAowh/XePh5JN8PTkNFS9eM+rnN9m8WX08XYFe30kLYw==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.4.1.tgz", + "integrity": "sha512-VsDZoTRWsXMeXRSF5eDD+WQDt7gXZ0nssg0GOELkk8kQ+AWe5rtzyDarRPCBO6t63kVaaslcE7er30F/k9m1wg==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8610,10 +8610,10 @@ } }, "node_modules/@dicebear/big-smile": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.2.4.tgz", - "integrity": "sha512-zeEfXOOXy7j9tfkPLzfQdLBPyQsctBetTdEfKRArc1k3RUliNPxfJG9j88+cXQC6GXrVW2pcT2X50NSPtugCFQ==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.4.1.tgz", + "integrity": "sha512-II+/4AIuf6StMAXz8xGjenHRfYkwuJlZM0dFGGxHHQR4Cr5h4+lJ74BeH0vxpCbCe0LZpPXbkxZ5qFB0IEXltQ==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8622,10 +8622,10 @@ } }, "node_modules/@dicebear/bottts": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.2.4.tgz", - "integrity": "sha512-4CTqrnVg+NQm6lZ4UuCJish8gGWe8EqSJrzvHQRO5TEyAKjYxbTdVqejpkycG1xkawha4FfxsYgtlSx7UwoVMw==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.4.1.tgz", + "integrity": "sha512-VgzXdRN+685i8MJ16xfw7ly6jKWqUkDnTv61cb6kkvSLfUTmJopYU0K8YWGAlURWAJux/IYA7EDh6SQ6AnACxQ==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -8634,10 +8634,10 @@ } }, "node_modules/@dicebear/bottts-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.2.4.tgz", - "integrity": "sha512-eMVdofdD/udHsKIaeWEXShDRtiwk7vp4FjY7l0f79vIzfhkIsXKEhPcnvHKOl/yoArlDVS3Uhgjj0crWTO9RJA==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.4.1.tgz", + "integrity": "sha512-53wdnsvi9RjOmaOo3tA5bUZU0azUVQaF90JjhABmmX1mwJ2lHJXh+Np6X/PmTkZ+2zLVjnhQKZ0KRfX/Um+S+g==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -8646,41 +8646,42 @@ } }, "node_modules/@dicebear/collection": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.2.4.tgz", - "integrity": "sha512-I1wCUp0yu5qSIeMQHmDYXQIXKkKjcja/SYBxppPkYFXpR2alxb0k9/swFDdMbkY6a1c9AT1kI1y+Pg6ywQ2rTA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.4.1.tgz", + "integrity": "sha512-sgu4JGrpyJmxB+LdUvSy0iYfdlTRbuyaKozS62Q8+FYWKIVkrcKpeJjD+6kwDKaBozAsa/8zgH3RhhxfkhROcA==", "license": "MIT", "dependencies": { - "@dicebear/adventurer": "9.2.4", - "@dicebear/adventurer-neutral": "9.2.4", - "@dicebear/avataaars": "9.2.4", - "@dicebear/avataaars-neutral": "9.2.4", - "@dicebear/big-ears": "9.2.4", - "@dicebear/big-ears-neutral": "9.2.4", - "@dicebear/big-smile": "9.2.4", - "@dicebear/bottts": "9.2.4", - "@dicebear/bottts-neutral": "9.2.4", - "@dicebear/croodles": "9.2.4", - "@dicebear/croodles-neutral": "9.2.4", - "@dicebear/dylan": "9.2.4", - "@dicebear/fun-emoji": "9.2.4", - "@dicebear/glass": "9.2.4", - "@dicebear/icons": "9.2.4", - "@dicebear/identicon": "9.2.4", - "@dicebear/initials": "9.2.4", - "@dicebear/lorelei": "9.2.4", - "@dicebear/lorelei-neutral": "9.2.4", - "@dicebear/micah": "9.2.4", - "@dicebear/miniavs": "9.2.4", - "@dicebear/notionists": "9.2.4", - "@dicebear/notionists-neutral": "9.2.4", - "@dicebear/open-peeps": "9.2.4", - "@dicebear/personas": "9.2.4", - "@dicebear/pixel-art": "9.2.4", - "@dicebear/pixel-art-neutral": "9.2.4", - "@dicebear/rings": "9.2.4", - "@dicebear/shapes": "9.2.4", - "@dicebear/thumbs": "9.2.4" + "@dicebear/adventurer": "9.4.1", + "@dicebear/adventurer-neutral": "9.4.1", + "@dicebear/avataaars": "9.4.1", + "@dicebear/avataaars-neutral": "9.4.1", + "@dicebear/big-ears": "9.4.1", + "@dicebear/big-ears-neutral": "9.4.1", + "@dicebear/big-smile": "9.4.1", + "@dicebear/bottts": "9.4.1", + "@dicebear/bottts-neutral": "9.4.1", + "@dicebear/croodles": "9.4.1", + "@dicebear/croodles-neutral": "9.4.1", + "@dicebear/dylan": "9.4.1", + "@dicebear/fun-emoji": "9.4.1", + "@dicebear/glass": "9.4.1", + "@dicebear/icons": "9.4.1", + "@dicebear/identicon": "9.4.1", + "@dicebear/initials": "9.4.1", + "@dicebear/lorelei": "9.4.1", + "@dicebear/lorelei-neutral": "9.4.1", + "@dicebear/micah": "9.4.1", + "@dicebear/miniavs": "9.4.1", + "@dicebear/notionists": "9.4.1", + "@dicebear/notionists-neutral": "9.4.1", + "@dicebear/open-peeps": "9.4.1", + "@dicebear/personas": "9.4.1", + "@dicebear/pixel-art": "9.4.1", + "@dicebear/pixel-art-neutral": "9.4.1", + "@dicebear/rings": "9.4.1", + "@dicebear/shapes": "9.4.1", + "@dicebear/thumbs": "9.4.1", + "@dicebear/toon-head": "9.4.1" }, "engines": { "node": ">=18.0.0" @@ -8690,22 +8691,22 @@ } }, "node_modules/@dicebear/core": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.2.4.tgz", - "integrity": "sha512-hz6zArEcUwkZzGOSJkWICrvqnEZY7BKeiq9rqKzVJIc1tRVv0MkR0FGvIxSvXiK9TTIgKwu656xCWAGAl6oh+w==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.1.tgz", + "integrity": "sha512-yzmoEhAc6CTaY9v0xz4MI3FTt5I5O+cvphpE+kd6Qz8XjW3/YveXEQcdO4CW0CTSUao88a8+/IMvnMGfUmDW1Q==", "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.11" + "@types/json-schema": "^7.0.15" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@dicebear/croodles": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.2.4.tgz", - "integrity": "sha512-CqT0NgVfm+5kd+VnjGY4WECNFeOrj5p7GCPTSEA7tCuN72dMQOX47P9KioD3wbExXYrIlJgOcxNrQeb/FMGc3A==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.4.1.tgz", + "integrity": "sha512-N1LQRi45JUIawKRMTYDuUbQMxUwcGUULcdUkGy1oCgTwnjnbFhZ2+hIsSTghyrjE44U8N3Rc8msTOzIVkioiYA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8714,10 +8715,10 @@ } }, "node_modules/@dicebear/croodles-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.2.4.tgz", - "integrity": "sha512-8vAS9lIEKffSUVx256GSRAlisB8oMX38UcPWw72venO/nitLVsyZ6hZ3V7eBdII0Onrjqw1RDndslQODbVcpTw==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.4.1.tgz", + "integrity": "sha512-FkA29zAvWKZF8DYIIBGedsqNpIUmj4/NOxcEHgxhWH4AxW6zwv0gZ29lbQ0tUlDah7yQfbV7beLepwr2pVYYZQ==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8726,10 +8727,10 @@ } }, "node_modules/@dicebear/dylan": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.2.4.tgz", - "integrity": "sha512-tiih1358djAq0jDDzmW3N3S4C3ynC2yn4hhlTAq/MaUAQtAi47QxdHdFGdxH0HBMZKqA4ThLdVk3yVgN4xsukg==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.4.1.tgz", + "integrity": "sha512-CX2lEJ3nXjWnp18VIDM++be36qFVz8yUpNvf7K5Ehh//tmfGZ/GjdTWpXk79baNM5JgUEnzCMHpAM0IU3jt8rQ==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8738,10 +8739,10 @@ } }, "node_modules/@dicebear/fun-emoji": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.2.4.tgz", - "integrity": "sha512-Od729skczse1HvHekgEFv+mSuJKMC4sl5hENGi/izYNe6DZDqJrrD0trkGT/IVh/SLXUFbq1ZFY9I2LoUGzFZg==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.4.1.tgz", + "integrity": "sha512-ys9Q/wCZt48bFlGbHQPLrwGnOhe40fPxoHH6n9NLKoWXEEoXE7wExQje40FGDFOSk7Ja+PvvYDkTd365AMRGEA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8750,9 +8751,9 @@ } }, "node_modules/@dicebear/glass": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.2.4.tgz", - "integrity": "sha512-5lxbJode1t99eoIIgW0iwZMoZU4jNMJv/6vbsgYUhAslYFX5zP0jVRscksFuo89TTtS7YKqRqZAL3eNhz4bTDw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.4.1.tgz", + "integrity": "sha512-W5zFrlZxa0UHDKUWwAVdZA6H42fOZ1uQC4mlt4dPOV7ZAYmx1ZcfvHYGeNuY87fJaeD9RZd+FnIGKRKAZdwf+g==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8762,9 +8763,9 @@ } }, "node_modules/@dicebear/icons": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.2.4.tgz", - "integrity": "sha512-bRsK1qj8u9Z76xs8XhXlgVr/oHh68tsHTJ/1xtkX9DeTQTSamo2tS26+r231IHu+oW3mePtFnwzdG9LqEPRd4A==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.4.1.tgz", + "integrity": "sha512-O7LIY8ksjAvWfW9o4ImFbzd8kFEXJet7LRMTK9RXU6ch9WZwiqNlrAB0oVkKI1aRAH2CM7wSfuTUjQxQF3BTVA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8774,9 +8775,9 @@ } }, "node_modules/@dicebear/identicon": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.2.4.tgz", - "integrity": "sha512-R9nw/E8fbu9HltHOqI9iL/o9i7zM+2QauXWMreQyERc39oGR9qXiwgBxsfYGcIS4C85xPyuL5B3I2RXrLBlJPg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.4.1.tgz", + "integrity": "sha512-xzIh8znm/OGAq6WHIkapn3pvjC+oEp7b9nyWSwuFmt5ESVKVO+ppD3TCDOe9Q5ZFXdL9ZijmdCR28Hvi5Ku/PA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8786,9 +8787,9 @@ } }, "node_modules/@dicebear/initials": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.2.4.tgz", - "integrity": "sha512-4SzHG5WoQZl1TGcpEZR4bdsSkUVqwNQCOwWSPAoBJa3BNxbVsvL08LF7I97BMgrCoknWZjQHUYt05amwTPTKtg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.4.1.tgz", + "integrity": "sha512-DnTK2Du3CIVCSqER80VgfMl47sa6eIHyq1kSkd9Y9D+ClfD8WDxpyHw8iOVGQ1nro7lIr7SfBb9P1aTNK0+FuA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8798,9 +8799,9 @@ } }, "node_modules/@dicebear/lorelei": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.2.4.tgz", - "integrity": "sha512-eS4mPYUgDpo89HvyFAx/kgqSSKh8W4zlUA8QJeIUCWTB0WpQmeqkSgIyUJjGDYSrIujWi+zEhhckksM5EwW0Dg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.4.1.tgz", + "integrity": "sha512-bB1N8yFdumo3G/3N91L4mismx50PQx4Gu8JwhN2UDH+aHpZ222TAJNr4xe5d6lrIB06VzlNhgLBANoUKRW6Wcg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8810,9 +8811,9 @@ } }, "node_modules/@dicebear/lorelei-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.2.4.tgz", - "integrity": "sha512-bWq2/GonbcJULtT+B/MGcM2UnA7kBQoH+INw8/oW83WI3GNTZ6qEwe3/W4QnCgtSOhUsuwuiSULguAFyvtkOZQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.4.1.tgz", + "integrity": "sha512-VunhzhsNmccxNiaSvQ8pL4/ZluMHJ1H7B69irwLJm+SqjyoWrjVUg0FFoJ8ZEkx0j6LSlPKr9nRxXy02Sk73Ng==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8822,10 +8823,10 @@ } }, "node_modules/@dicebear/micah": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.2.4.tgz", - "integrity": "sha512-XNWJ8Mx+pncIV8Ye0XYc/VkMiax8kTxcP3hLTC5vmELQyMSLXzg/9SdpI+W/tCQghtPZRYTT3JdY9oU9IUlP2g==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.4.1.tgz", + "integrity": "sha512-7yatVxu1k6NKNe4SeJwr1j8hBEZT2Eftmk1LPL8OOMwFNfm4/tY9ZQMf9T5bOBkOGIDH0mvY8rw7kq99PgPNGg==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8834,10 +8835,10 @@ } }, "node_modules/@dicebear/miniavs": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.2.4.tgz", - "integrity": "sha512-k7IYTAHE/4jSO6boMBRrNlqPT3bh7PLFM1atfe0nOeCDwmz/qJUBP3HdONajbf3fmo8f2IZYhELrNWTOE7Ox3Q==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.4.1.tgz", + "integrity": "sha512-35/koev3bsDPchre9xjCY+QgyKUTl+TdyuBp0Ve2ixbE/Ywe5BSGwK4Uixon7ZA4+XEivpF9KNEn+9FSQLGHCQ==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8846,9 +8847,9 @@ } }, "node_modules/@dicebear/notionists": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.2.4.tgz", - "integrity": "sha512-zcvpAJ93EfC0xQffaPZQuJPShwPhnu9aTcoPsaYGmw0oEDLcv2XYmDhUUdX84QYCn6LtCZH053rHLVazRW+OGw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.4.1.tgz", + "integrity": "sha512-AQLwB1nyePPHF2voI+f6u/Rqt5az+deMdxG9XeVTNrGc57L5dkD+hSAryELcp2Zjn45kL/3CX2aZb2usdXtdQA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8858,9 +8859,9 @@ } }, "node_modules/@dicebear/notionists-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.2.4.tgz", - "integrity": "sha512-fskWzBVxQzJhCKqY24DGZbYHSBaauoRa1DgXM7+7xBuksH7mfbTmZTvnUAsAqJYBkla8IPb4ERKduDWtlWYYjQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.4.1.tgz", + "integrity": "sha512-sCgf3T08az1mFQj6mlrgIh5pFmiBbqBhJXVA75uCd56g9UiXH8BG1eraIqYYMRpHX6JF2FHC8EGcmtqPNnl9Bg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8870,9 +8871,9 @@ } }, "node_modules/@dicebear/open-peeps": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.2.4.tgz", - "integrity": "sha512-s6nwdjXFsplqEI7imlsel4Gt6kFVJm6YIgtZSpry0UdwDoxUUudei5bn957j9lXwVpVUcRjJW+TuEKztYjXkKQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.4.1.tgz", + "integrity": "sha512-pdtttjRm55PNBk43K4nIXy07VWWcX7Ds4iAk0VyPhYkXN6ndDDMtrkxVxSeroO1LswMD58MTfc0D9Iv7iyI9SA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8882,10 +8883,10 @@ } }, "node_modules/@dicebear/personas": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.2.4.tgz", - "integrity": "sha512-JNim8RfZYwb0MfxW6DLVfvreCFIevQg+V225Xe5tDfbFgbcYEp4OU/KaiqqO2476OBjCw7i7/8USbv2acBhjwA==", - "license": "MIT", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.4.1.tgz", + "integrity": "sha512-3gVfj3ST/kDhg1GBd67rAiWUpg3+ZFm4lsxu6m6bjgEHff6dR6Mu3zl2sjz/wc+iqJjO30EFIkHgJU06Dwah2g==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -8894,9 +8895,9 @@ } }, "node_modules/@dicebear/pixel-art": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.2.4.tgz", - "integrity": "sha512-4Ao45asieswUdlCTBZqcoF/0zHR3OWUWB0Mvhlu9b1Fbc6IlPBiOfx2vsp6bnVGVnMag58tJLecx2omeXdECBQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.4.1.tgz", + "integrity": "sha512-0CfWusT9nZp/s4bBuqJWhDLFtIGVGANDq5+X8R58wvXC0AscyxJfpVgz5v0rNacr55JbiQIqHLlU/ZpxDSBeqg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8906,9 +8907,9 @@ } }, "node_modules/@dicebear/pixel-art-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.2.4.tgz", - "integrity": "sha512-ZITPLD1cPN4GjKkhWi80s7e5dcbXy34ijWlvmxbc4eb/V7fZSsyRa9EDUW3QStpo+xrCJLcLR+3RBE5iz0PC/A==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.4.1.tgz", + "integrity": "sha512-DFSCVZCUA6IGs+jcgxPbTdbWbjOD/YbAn1cvipSaO1zSRNl6xkgYJyCBkqPAiiKgQ1Fc1DhkrRUEXF/2YeC5LA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8918,9 +8919,9 @@ } }, "node_modules/@dicebear/rings": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.2.4.tgz", - "integrity": "sha512-teZxELYyV2ogzgb5Mvtn/rHptT0HXo9SjUGS4A52mOwhIdHSGGU71MqA1YUzfae9yJThsw6K7Z9kzuY2LlZZHA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.4.1.tgz", + "integrity": "sha512-L+rJt94B2a4xT8dyaz7TEqkSKEvvNGO1uTNGSUaM5YPM9DU9dNdnE30VC+MoXVE6sfcMSGKbxWsbzwEx49Y8Kg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8930,9 +8931,9 @@ } }, "node_modules/@dicebear/shapes": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.2.4.tgz", - "integrity": "sha512-MhK9ZdFm1wUnH4zWeKPRMZ98UyApolf5OLzhCywfu38tRN6RVbwtBRHc/42ZwoN1JU1JgXr7hzjYucMqISHtbA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.4.1.tgz", + "integrity": "sha512-uBomOUBNhhVyEvGcAyo+Fj939ZYdpWnvz95/71Kkh3FdTB9ZNeLongz0jmy1t/UJyJooTgIQFYTks/08FDuEJg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8942,9 +8943,9 @@ } }, "node_modules/@dicebear/thumbs": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.2.4.tgz", - "integrity": "sha512-EL4sMqv9p2+1Xy3d8e8UxyeKZV2+cgt3X2x2RTRzEOIIhobtkL8u6lJxmJbiGbpVtVALmrt5e7gjmwqpryYDpg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.4.1.tgz", + "integrity": "sha512-vJsXw7qoCeFht/RFE3NK9mKxhWv91vE232Au33Ypq1DRg8gLtoC+3RMTHj5mfQVhrObGgv/HTCTOrQw+9l4phg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8953,6 +8954,18 @@ "@dicebear/core": "^9.0.0" } }, + "node_modules/@dicebear/toon-head": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@dicebear/toon-head/-/toon-head-9.4.1.tgz", + "integrity": "sha512-cBpH4p5cH+CWu4cPvTgqWZLyTqrTQq9/hN22JvHyqugehzCYic2B2a1+mUstixIv5LDqPYP/4pEzbE3cjqmjYw==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", @@ -43947,8 +43960,8 @@ "peerDependencies": { "@ariakit/react": "^0.4.16", "@ariakit/react-core": "^0.4.17", - "@dicebear/collection": "^9.2.2", - "@dicebear/core": "^9.2.2", + "@dicebear/collection": "^9.4.1", + "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "1.0.2", diff --git a/packages/client/package.json b/packages/client/package.json index e76c1d075a..908f9f98f7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -31,8 +31,8 @@ "peerDependencies": { "@ariakit/react": "^0.4.16", "@ariakit/react-core": "^0.4.17", - "@dicebear/collection": "^9.2.2", - "@dicebear/core": "^9.2.2", + "@dicebear/collection": "^9.4.1", + "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "1.0.2", From ec0238d7cad8dbeab27d56be58d43b2059e9e8d9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 17:07:08 -0400 Subject: [PATCH 073/111] =?UTF-8?q?=F0=9F=90=B3=20chore:=20Upgrade=20Alpin?= =?UTF-8?q?e=20packages=20in=20Dockerfiles=20(#12316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `apk upgrade --no-cache` to both Dockerfile and Dockerfile.multi to ensure all installed packages are up to date. - Maintained the installation of `jemalloc` and other dependencies without changes. --- Dockerfile | 2 +- Dockerfile.multi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 02bda8a589..3c4963f970 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # Base node image FROM node:20-alpine AS node -# Install jemalloc +RUN apk upgrade --no-cache RUN apk add --no-cache jemalloc RUN apk add --no-cache python3 py3-pip uv diff --git a/Dockerfile.multi b/Dockerfile.multi index 8e7483e378..bc4203f265 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -6,7 +6,7 @@ ARG NODE_MAX_OLD_SPACE_SIZE=6144 # Base for all builds FROM node:20-alpine AS base-min -# Install jemalloc +RUN apk upgrade --no-cache RUN apk add --no-cache jemalloc # Set environment variable to use jemalloc ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2 From f380390408ffe0ae1470d4c5217224e7de656947 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 17:15:12 -0400 Subject: [PATCH 074/111] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20Prevent?= =?UTF-8?q?=20loop=20in=20ChatGPT=20import=20on=20Cyclic=20Parent=20Graphs?= =?UTF-8?q?=20(#12313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cap adjustTimestampsForOrdering to N passes and add cycle detection to findValidParent, preventing DoS via crafted ChatGPT export files with cyclic parentMessageId relationships. Add breakParentCycles to sever cyclic back-edges before saving, ensuring structurally valid message trees are persisted to the DB. --- .../utils/import/importers-timestamp.spec.js | 128 ++++++++++++++++++ api/server/utils/import/importers.js | 117 ++++++++++++---- 2 files changed, 219 insertions(+), 26 deletions(-) diff --git a/api/server/utils/import/importers-timestamp.spec.js b/api/server/utils/import/importers-timestamp.spec.js index c7665dfe25..02f24f72ae 100644 --- a/api/server/utils/import/importers-timestamp.spec.js +++ b/api/server/utils/import/importers-timestamp.spec.js @@ -1,3 +1,4 @@ +const { logger } = require('@librechat/data-schemas'); const { Constants } = require('librechat-data-provider'); const { ImportBatchBuilder } = require('./importBatchBuilder'); const { getImporter } = require('./importers'); @@ -368,6 +369,133 @@ describe('Import Timestamp Ordering', () => { new Date(nullTimeMsg.createdAt).getTime(), ); }); + + test('should terminate on cyclic parent relationships and break cycles before saving', async () => { + const warnSpy = jest.spyOn(logger, 'warn'); + const jsonData = [ + { + title: 'Cycle Test', + create_time: 1700000000, + mapping: { + 'root-node': { + id: 'root-node', + message: null, + parent: null, + children: ['message-a'], + }, + 'message-a': { + id: 'message-a', + message: { + id: 'message-a', + author: { role: 'user' }, + create_time: 1700000000, + content: { content_type: 'text', parts: ['Message A'] }, + metadata: {}, + }, + parent: 'message-b', + children: ['message-b'], + }, + 'message-b': { + id: 'message-b', + message: { + id: 'message-b', + author: { role: 'assistant' }, + create_time: 1700000000, + content: { content_type: 'text', parts: ['Message B'] }, + metadata: {}, + }, + parent: 'message-a', + children: ['message-a'], + }, + }, + }, + ]; + + const requestUserId = 'user-123'; + const importBatchBuilder = new ImportBatchBuilder(requestUserId); + + const importer = getImporter(jsonData); + await importer(jsonData, requestUserId, () => importBatchBuilder); + + const { messages } = importBatchBuilder; + expect(messages).toHaveLength(2); + + const msgA = messages.find((m) => m.text === 'Message A'); + const msgB = messages.find((m) => m.text === 'Message B'); + expect(msgA).toBeDefined(); + expect(msgB).toBeDefined(); + + const roots = messages.filter((m) => m.parentMessageId === Constants.NO_PARENT); + expect(roots).toHaveLength(1); + + const [root] = roots; + const nonRoot = messages.find((m) => m.parentMessageId !== Constants.NO_PARENT); + expect(nonRoot.parentMessageId).toBe(root.messageId); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('cyclic parent relationships')); + warnSpy.mockRestore(); + }); + + test('should not hang when findValidParent encounters a skippable-message cycle', async () => { + const jsonData = [ + { + title: 'Skippable Cycle Test', + create_time: 1700000000, + mapping: { + 'root-node': { + id: 'root-node', + message: null, + parent: null, + children: ['real-msg'], + }, + 'sys-a': { + id: 'sys-a', + message: { + id: 'sys-a', + author: { role: 'system' }, + create_time: 1700000000, + content: { content_type: 'text', parts: ['system a'] }, + metadata: {}, + }, + parent: 'sys-b', + children: ['real-msg'], + }, + 'sys-b': { + id: 'sys-b', + message: { + id: 'sys-b', + author: { role: 'system' }, + create_time: 1700000000, + content: { content_type: 'text', parts: ['system b'] }, + metadata: {}, + }, + parent: 'sys-a', + children: [], + }, + 'real-msg': { + id: 'real-msg', + message: { + id: 'real-msg', + author: { role: 'user' }, + create_time: 1700000001, + content: { content_type: 'text', parts: ['Hello'] }, + metadata: {}, + }, + parent: 'sys-a', + children: [], + }, + }, + }, + ]; + + const importBatchBuilder = new ImportBatchBuilder('user-123'); + const importer = getImporter(jsonData); + await importer(jsonData, 'user-123', () => importBatchBuilder); + + const realMsg = importBatchBuilder.messages.find((m) => m.text === 'Hello'); + expect(realMsg).toBeDefined(); + expect(realMsg.parentMessageId).toBe(Constants.NO_PARENT); + }); }); describe('Comparison with Fork Functionality', () => { diff --git a/api/server/utils/import/importers.js b/api/server/utils/import/importers.js index 81a0f048df..39734c181c 100644 --- a/api/server/utils/import/importers.js +++ b/api/server/utils/import/importers.js @@ -324,32 +324,42 @@ function processConversation(conv, importBatchBuilder, requestUserId) { } /** - * Helper function to find the nearest valid parent (skips system, reasoning_recap, and thoughts messages) - * @param {string} parentId - The ID of the parent message. + * Finds the nearest valid parent by traversing up through skippable messages + * (system, reasoning_recap, thoughts). Uses iterative traversal to avoid + * stack overflow on deep chains of skippable messages. + * + * @param {string} startId - The ID of the starting parent message. * @returns {string} The ID of the nearest valid parent message. */ - const findValidParent = (parentId) => { - if (!parentId || !messageMap.has(parentId)) { - return Constants.NO_PARENT; + const findValidParent = (startId) => { + const visited = new Set(); + let parentId = startId; + + while (parentId) { + if (!messageMap.has(parentId) || visited.has(parentId)) { + return Constants.NO_PARENT; + } + visited.add(parentId); + + const parentMapping = conv.mapping[parentId]; + if (!parentMapping?.message) { + return Constants.NO_PARENT; + } + + const contentType = parentMapping.message.content?.content_type; + const shouldSkip = + parentMapping.message.author?.role === 'system' || + contentType === 'reasoning_recap' || + contentType === 'thoughts'; + + if (!shouldSkip) { + return messageMap.get(parentId); + } + + parentId = parentMapping.parent; } - const parentMapping = conv.mapping[parentId]; - if (!parentMapping?.message) { - return Constants.NO_PARENT; - } - - /* If parent is a system message, reasoning_recap, or thoughts, traverse up to find the nearest valid parent */ - const contentType = parentMapping.message.content?.content_type; - const shouldSkip = - parentMapping.message.author?.role === 'system' || - contentType === 'reasoning_recap' || - contentType === 'thoughts'; - - if (shouldSkip) { - return findValidParent(parentMapping.parent); - } - - return messageMap.get(parentId); + return Constants.NO_PARENT; }; /** @@ -466,7 +476,10 @@ function processConversation(conv, importBatchBuilder, requestUserId) { messages.push(message); } - adjustTimestampsForOrdering(messages); + const cycleDetected = adjustTimestampsForOrdering(messages); + if (cycleDetected) { + breakParentCycles(messages); + } for (const message of messages) { importBatchBuilder.saveMessage(message); @@ -553,21 +566,30 @@ function formatMessageText(messageData) { * Messages are sorted by createdAt and buildTree expects parents to appear before children. * ChatGPT exports can have slight timestamp inversions (e.g., tool call results * arriving a few ms before their parent). Uses multiple passes to handle cascading adjustments. + * Capped at N passes (where N = message count) to guarantee termination on cyclic graphs. * * @param {Array} messages - Array of message objects with messageId, parentMessageId, and createdAt. + * @returns {boolean} True if cyclic parent relationships were detected. */ function adjustTimestampsForOrdering(messages) { + if (messages.length === 0) { + return false; + } + const timestampMap = new Map(); - messages.forEach((msg) => timestampMap.set(msg.messageId, msg.createdAt)); + for (const msg of messages) { + timestampMap.set(msg.messageId, msg.createdAt); + } let hasChanges = true; - while (hasChanges) { + let remainingPasses = messages.length; + while (hasChanges && remainingPasses > 0) { hasChanges = false; + remainingPasses--; for (const message of messages) { if (message.parentMessageId && message.parentMessageId !== Constants.NO_PARENT) { const parentTimestamp = timestampMap.get(message.parentMessageId); if (parentTimestamp && message.createdAt <= parentTimestamp) { - // Bump child timestamp to 1ms after parent message.createdAt = new Date(parentTimestamp.getTime() + 1); timestampMap.set(message.messageId, message.createdAt); hasChanges = true; @@ -575,6 +597,49 @@ function adjustTimestampsForOrdering(messages) { } } } + + const cycleDetected = remainingPasses === 0 && hasChanges; + if (cycleDetected) { + logger.warn( + '[importers] Detected cyclic parent relationships while adjusting import timestamps', + ); + } + return cycleDetected; +} + +/** + * Severs cyclic parentMessageId back-edges so saved messages form a valid tree. + * Walks each message's parent chain; if a message is visited twice, its parentMessageId + * is set to NO_PARENT to break the cycle. + * + * @param {Array} messages - Array of message objects with messageId and parentMessageId. + */ +function breakParentCycles(messages) { + const parentLookup = new Map(); + for (const msg of messages) { + parentLookup.set(msg.messageId, msg); + } + + const settled = new Set(); + for (const message of messages) { + const chain = new Set(); + let current = message; + while (current && !settled.has(current.messageId)) { + if (chain.has(current.messageId)) { + current.parentMessageId = Constants.NO_PARENT; + break; + } + chain.add(current.messageId); + const parentId = current.parentMessageId; + if (!parentId || parentId === Constants.NO_PARENT) { + break; + } + current = parentLookup.get(parentId); + } + for (const id of chain) { + settled.add(id); + } + } } module.exports = { getImporter, processAssistantMessage }; From 1ecff83b20f637cb95b2d07686c8abe38f472466 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 17:46:14 -0400 Subject: [PATCH 075/111] =?UTF-8?q?=F0=9F=AA=A6=20fix:=20ACL-Safe=20User?= =?UTF-8?q?=20Account=20Deletion=20for=20Agents,=20Prompts,=20and=20MCP=20?= =?UTF-8?q?Servers=20(#12314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: use ACL ownership for prompt group cleanup on user deletion deleteUserPrompts previously called getAllPromptGroups with only an author filter, which defaults to searchShared=true and drops the author filter for shared/global project entries. This caused any user deleting their account to strip shared prompt group associations and ACL entries for other users. Replace the author-based query with ACL-based ownership lookup: - Find prompt groups where the user has OWNER permission (DELETE bit) - Only delete groups where the user is the sole owner - Preserve multi-owned groups and their ACL entries for other owners * fix: use ACL ownership for agent cleanup on user deletion deleteUserAgents used the deprecated author field to find and delete agents, then unconditionally removed all ACL entries for those agents. This could destroy ACL entries for agents shared with or co-owned by other users. Replace the author-based query with ACL-based ownership lookup: - Find agents where the user has OWNER permission (DELETE bit) - Only delete agents where the user is the sole owner - Preserve multi-owned agents and their ACL entries for other owners - Also clean up handoff edges referencing deleted agents * fix: add MCP server cleanup on user deletion User deletion had no cleanup for MCP servers, leaving solely-owned servers orphaned in the database with dangling ACL entries for other users. Add deleteUserMcpServers that follows the same ACL ownership pattern as prompt groups and agents: find servers with OWNER permission, check for sole ownership, and only delete those with no other owners. * style: fix prettier formatting in Prompt.spec.js * refactor: extract getSoleOwnedResourceIds to PermissionService The ACL sole-ownership detection algorithm was duplicated across deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers. Centralizes the three-step pattern (find owned entries, find other owners, compute sole-owned set) into a single reusable utility. * refactor: use getSoleOwnedResourceIds in all deletion functions - Replace inline ACL queries with the centralized utility - Remove vestigial _req parameter from deleteUserPrompts - Use Promise.all for parallel project removal instead of sequential awaits - Disconnect live MCP sessions and invalidate tool cache before deleting sole-owned MCP server documents - Export deleteUserMcpServers for testability * test: improve deletion test coverage and quality - Move deleteUserPrompts call to beforeAll to eliminate execution-order dependency between tests - Standardize on test() instead of it() for consistency in Prompt.spec.js - Add assertion for deleting user's own ACL entry preservation on multi-owned agents - Add deleteUserMcpServers integration test suite with 6 tests covering sole-owner deletion, multi-owner preservation, session disconnect, cache invalidation, model-not-registered guard, and missing MCPManager - Add PermissionService mock to existing deleteUser.spec.js to fix import chain * fix: add legacy author-based fallback for unmigrated resources Resources created before the ACL system have author set but no AclEntry records. The sole-ownership detection returns empty for these, causing deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers to silently skip them — permanently orphaning data on user deletion. Add a fallback that identifies author-owned resources with zero ACL entries (truly unmigrated) and includes them in the deletion set. This preserves the multi-owner safety of the ACL path while ensuring pre-ACL resources are still cleaned up regardless of migration status. * style: fix prettier formatting across all changed files * test: add resource type coverage guard for user deletion Ensures every ResourceType in the ACL system has a corresponding cleanup handler wired into deleteUserController. When a new ResourceType is added (e.g. WORKFLOW), this test fails immediately, preventing silent data orphaning on user account deletion. * style: fix import order in PermissionService destructure * test: add opt-out set and fix test lifecycle in coverage guard Add NO_USER_CLEANUP_NEEDED set for resource types that legitimately require no per-user deletion. Move fs.readFileSync into beforeAll so path errors surface as clean test failures instead of unhandled crashes. --- api/models/Agent.js | 65 +++- api/models/Agent.spec.js | 199 +++++++++-- api/models/Prompt.js | 49 ++- api/models/Prompt.spec.js | 227 +++++++++++++ api/server/controllers/UserController.js | 86 ++++- .../controllers/__tests__/deleteUser.spec.js | 4 + .../__tests__/deleteUserMcpServers.spec.js | 319 ++++++++++++++++++ .../deleteUserResourceCoverage.spec.js | 53 +++ api/server/services/PermissionService.js | 51 ++- 9 files changed, 993 insertions(+), 60 deletions(-) create mode 100644 api/server/controllers/__tests__/deleteUserMcpServers.spec.js create mode 100644 api/server/controllers/__tests__/deleteUserResourceCoverage.spec.js diff --git a/api/models/Agent.js b/api/models/Agent.js index 663285183a..53098888d6 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -17,7 +17,10 @@ const { removeAgentIdsFromProject, addAgentIdsToProject, } = require('./Project'); -const { removeAllPermissions } = require('~/server/services/PermissionService'); +const { + getSoleOwnedResourceIds, + removeAllPermissions, +} = require('~/server/services/PermissionService'); const { getMCPServerTools } = require('~/server/services/Config'); const { Agent, AclEntry, User } = require('~/db/models'); const { getActions } = require('./Action'); @@ -617,30 +620,70 @@ const deleteAgent = async (searchParameter) => { }; /** - * Deletes all agents created by a specific user. + * Deletes agents solely owned by the user and cleans up their ACLs/project references. + * Agents with other owners are left intact; the caller is responsible for + * removing the user's own ACL principal entries separately. + * + * Also handles legacy (pre-ACL) agents that only have the author field set, + * ensuring they are not orphaned if no permission migration has been run. * @param {string} userId - The ID of the user whose agents should be deleted. - * @returns {Promise} A promise that resolves when all user agents have been deleted. + * @returns {Promise} */ const deleteUserAgents = async (userId) => { try { - const userAgents = await getAgents({ author: userId }); + const userObjectId = new mongoose.Types.ObjectId(userId); + const soleOwnedObjectIds = await getSoleOwnedResourceIds(userObjectId, [ + ResourceType.AGENT, + ResourceType.REMOTE_AGENT, + ]); - if (userAgents.length === 0) { + const authoredAgents = await Agent.find({ author: userObjectId }).select('id _id').lean(); + + const migratedEntries = + authoredAgents.length > 0 + ? await AclEntry.find({ + resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, + resourceId: { $in: authoredAgents.map((a) => a._id) }, + }) + .select('resourceId') + .lean() + : []; + const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString())); + const legacyAgents = authoredAgents.filter((a) => !migratedIds.has(a._id.toString())); + + /** resourceId is the MongoDB _id; agent.id is the string identifier for project/edge queries */ + const soleOwnedAgents = + soleOwnedObjectIds.length > 0 + ? await Agent.find({ _id: { $in: soleOwnedObjectIds } }) + .select('id _id') + .lean() + : []; + + const allAgents = [...soleOwnedAgents, ...legacyAgents]; + + if (allAgents.length === 0) { return; } - const agentIds = userAgents.map((agent) => agent.id); - const agentObjectIds = userAgents.map((agent) => agent._id); + const agentIds = allAgents.map((agent) => agent.id); + const agentObjectIds = allAgents.map((agent) => agent._id); - for (const agentId of agentIds) { - await removeAgentFromAllProjects(agentId); - } + await Promise.all(agentIds.map((id) => removeAgentFromAllProjects(id))); await AclEntry.deleteMany({ resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, resourceId: { $in: agentObjectIds }, }); + try { + await Agent.updateMany( + { 'edges.to': { $in: agentIds } }, + { $pull: { edges: { to: { $in: agentIds } } } }, + ); + } catch (error) { + logger.error('[deleteUserAgents] Error removing agents from handoff edges', error); + } + try { await User.updateMany( { 'favorites.agentId': { $in: agentIds } }, @@ -650,7 +693,7 @@ const deleteUserAgents = async (userId) => { logger.error('[deleteUserAgents] Error removing agents from user favorites', error); } - await Agent.deleteMany({ author: userId }); + await Agent.deleteMany({ _id: { $in: agentObjectIds } }); } catch (error) { logger.error('[deleteUserAgents] General error:', error); } diff --git a/api/models/Agent.spec.js b/api/models/Agent.spec.js index baceb3e8f3..b2597872ab 100644 --- a/api/models/Agent.spec.js +++ b/api/models/Agent.spec.js @@ -15,7 +15,12 @@ const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); const { agentSchema } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); -const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); +const { + ResourceType, + AccessRoleIds, + PrincipalType, + PermissionBits, +} = require('librechat-data-provider'); const { getAgent, loadAgent, @@ -442,6 +447,7 @@ describe('models/Agent', () => { beforeEach(async () => { await Agent.deleteMany({}); + await AclEntry.deleteMany({}); }); test('should create and get an agent', async () => { @@ -838,8 +844,7 @@ describe('models/Agent', () => { const agent2Id = `agent_${uuidv4()}`; const otherAuthorAgentId = `agent_${uuidv4()}`; - // Create agents by the author to be deleted - await createAgent({ + const agent1 = await createAgent({ id: agent1Id, name: 'Author Agent 1', provider: 'test', @@ -847,7 +852,7 @@ describe('models/Agent', () => { author: authorId, }); - await createAgent({ + const agent2 = await createAgent({ id: agent2Id, name: 'Author Agent 2', provider: 'test', @@ -855,7 +860,6 @@ describe('models/Agent', () => { author: authorId, }); - // Create agent by different author (should not be deleted) await createAgent({ id: otherAuthorAgentId, name: 'Other Author Agent', @@ -864,7 +868,23 @@ describe('models/Agent', () => { author: otherAuthorId, }); - // Create user with all agents in favorites + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: authorId, + resourceType: ResourceType.AGENT, + resourceId: agent1._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: authorId, + }); + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: authorId, + resourceType: ResourceType.AGENT, + resourceId: agent2._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: authorId, + }); + await User.create({ _id: userId, name: 'Test User', @@ -878,21 +898,16 @@ describe('models/Agent', () => { ], }); - // Verify user has all favorites const userBefore = await User.findById(userId); expect(userBefore.favorites).toHaveLength(4); - // Delete all agents by the author await deleteUserAgents(authorId.toString()); - // Verify author's agents are deleted from database expect(await getAgent({ id: agent1Id })).toBeNull(); expect(await getAgent({ id: agent2Id })).toBeNull(); - // Verify other author's agent still exists expect(await getAgent({ id: otherAuthorAgentId })).not.toBeNull(); - // Verify user favorites: author's agents removed, others remain const userAfter = await User.findById(userId); expect(userAfter.favorites).toHaveLength(2); expect(userAfter.favorites.some((f) => f.agentId === agent1Id)).toBe(false); @@ -911,8 +926,7 @@ describe('models/Agent', () => { const agent2Id = `agent_${uuidv4()}`; const unrelatedAgentId = `agent_${uuidv4()}`; - // Create agents by the author - await createAgent({ + const agent1 = await createAgent({ id: agent1Id, name: 'Author Agent 1', provider: 'test', @@ -920,7 +934,7 @@ describe('models/Agent', () => { author: authorId, }); - await createAgent({ + const agent2 = await createAgent({ id: agent2Id, name: 'Author Agent 2', provider: 'test', @@ -928,7 +942,23 @@ describe('models/Agent', () => { author: authorId, }); - // Create users with various favorites configurations + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: authorId, + resourceType: ResourceType.AGENT, + resourceId: agent1._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: authorId, + }); + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: authorId, + resourceType: ResourceType.AGENT, + resourceId: agent2._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: authorId, + }); + await User.create({ _id: user1Id, name: 'User 1', @@ -953,10 +983,8 @@ describe('models/Agent', () => { favorites: [{ agentId: unrelatedAgentId }, { model: 'gpt-4', endpoint: 'openAI' }], }); - // Delete all agents by the author await deleteUserAgents(authorId.toString()); - // Verify all users' favorites are correctly updated const user1After = await User.findById(user1Id); expect(user1After.favorites).toHaveLength(0); @@ -965,7 +993,6 @@ describe('models/Agent', () => { expect(user2After.favorites.some((f) => f.agentId === agent1Id)).toBe(false); expect(user2After.favorites.some((f) => f.model === 'claude-3')).toBe(true); - // User 3 should be completely unaffected const user3After = await User.findById(user3Id); expect(user3After.favorites).toHaveLength(2); expect(user3After.favorites.some((f) => f.agentId === unrelatedAgentId)).toBe(true); @@ -979,8 +1006,7 @@ describe('models/Agent', () => { const existingAgentId = `agent_${uuidv4()}`; - // Create agent by different author - await createAgent({ + const existingAgent = await createAgent({ id: existingAgentId, name: 'Existing Agent', provider: 'test', @@ -988,7 +1014,15 @@ describe('models/Agent', () => { author: otherAuthorId, }); - // Create user with favorites + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: otherAuthorId, + resourceType: ResourceType.AGENT, + resourceId: existingAgent._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: otherAuthorId, + }); + await User.create({ _id: userId, name: 'Test User', @@ -997,13 +1031,10 @@ describe('models/Agent', () => { favorites: [{ agentId: existingAgentId }, { model: 'gpt-4', endpoint: 'openAI' }], }); - // Delete agents for user with no agents (should be a no-op) await deleteUserAgents(authorWithNoAgentsId.toString()); - // Verify existing agent still exists expect(await getAgent({ id: existingAgentId })).not.toBeNull(); - // Verify user favorites are unchanged const userAfter = await User.findById(userId); expect(userAfter.favorites).toHaveLength(2); expect(userAfter.favorites.some((f) => f.agentId === existingAgentId)).toBe(true); @@ -1017,8 +1048,7 @@ describe('models/Agent', () => { const agent1Id = `agent_${uuidv4()}`; const agent2Id = `agent_${uuidv4()}`; - // Create agents by the author - await createAgent({ + const agent1 = await createAgent({ id: agent1Id, name: 'Agent 1', provider: 'test', @@ -1026,7 +1056,7 @@ describe('models/Agent', () => { author: authorId, }); - await createAgent({ + const agent2 = await createAgent({ id: agent2Id, name: 'Agent 2', provider: 'test', @@ -1034,7 +1064,23 @@ describe('models/Agent', () => { author: authorId, }); - // Create user with favorites that don't include these agents + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: authorId, + resourceType: ResourceType.AGENT, + resourceId: agent1._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: authorId, + }); + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: authorId, + resourceType: ResourceType.AGENT, + resourceId: agent2._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: authorId, + }); + await User.create({ _id: userId, name: 'Test User', @@ -1043,23 +1089,112 @@ describe('models/Agent', () => { favorites: [{ model: 'gpt-4', endpoint: 'openAI' }], }); - // Verify agents exist expect(await getAgent({ id: agent1Id })).not.toBeNull(); expect(await getAgent({ id: agent2Id })).not.toBeNull(); - // Delete all agents by the author await deleteUserAgents(authorId.toString()); - // Verify agents are deleted expect(await getAgent({ id: agent1Id })).toBeNull(); expect(await getAgent({ id: agent2Id })).toBeNull(); - // Verify user favorites are unchanged const userAfter = await User.findById(userId); expect(userAfter.favorites).toHaveLength(1); expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true); }); + test('should preserve multi-owned agents when deleteUserAgents is called', async () => { + const deletingUserId = new mongoose.Types.ObjectId(); + const otherOwnerId = new mongoose.Types.ObjectId(); + + const soleOwnedId = `agent_${uuidv4()}`; + const multiOwnedId = `agent_${uuidv4()}`; + + const soleAgent = await createAgent({ + id: soleOwnedId, + name: 'Sole Owned Agent', + provider: 'test', + model: 'test-model', + author: deletingUserId, + }); + + const multiAgent = await createAgent({ + id: multiOwnedId, + name: 'Multi Owned Agent', + provider: 'test', + model: 'test-model', + author: deletingUserId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: deletingUserId, + resourceType: ResourceType.AGENT, + resourceId: soleAgent._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: deletingUserId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: deletingUserId, + resourceType: ResourceType.AGENT, + resourceId: multiAgent._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: deletingUserId, + }); + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: otherOwnerId, + resourceType: ResourceType.AGENT, + resourceId: multiAgent._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: otherOwnerId, + }); + + await deleteUserAgents(deletingUserId.toString()); + + expect(await getAgent({ id: soleOwnedId })).toBeNull(); + expect(await getAgent({ id: multiOwnedId })).not.toBeNull(); + + const soleAcl = await AclEntry.find({ + resourceType: ResourceType.AGENT, + resourceId: soleAgent._id, + }); + expect(soleAcl).toHaveLength(0); + + const multiAcl = await AclEntry.find({ + resourceType: ResourceType.AGENT, + resourceId: multiAgent._id, + principalId: otherOwnerId, + }); + expect(multiAcl).toHaveLength(1); + expect(multiAcl[0].permBits & PermissionBits.DELETE).toBeTruthy(); + + const deletingUserMultiAcl = await AclEntry.find({ + resourceType: ResourceType.AGENT, + resourceId: multiAgent._id, + principalId: deletingUserId, + }); + expect(deletingUserMultiAcl).toHaveLength(1); + }); + + test('should delete legacy agents that have author but no ACL entries', async () => { + const legacyUserId = new mongoose.Types.ObjectId(); + const legacyAgentId = `agent_${uuidv4()}`; + + await createAgent({ + id: legacyAgentId, + name: 'Legacy Agent (no ACL)', + provider: 'test', + model: 'test-model', + author: legacyUserId, + }); + + await deleteUserAgents(legacyUserId.toString()); + + expect(await getAgent({ id: legacyAgentId })).toBeNull(); + }); + test('should update agent projects', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); diff --git a/api/models/Prompt.js b/api/models/Prompt.js index bde911b23a..4b14edbc74 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -13,7 +13,10 @@ const { addGroupIdsToProject, getProjectByName, } = require('./Project'); -const { removeAllPermissions } = require('~/server/services/PermissionService'); +const { + getSoleOwnedResourceIds, + removeAllPermissions, +} = require('~/server/services/PermissionService'); const { PromptGroup, Prompt, AclEntry } = require('~/db/models'); /** @@ -592,31 +595,49 @@ module.exports = { } }, /** - * Delete all prompts and prompt groups created by a specific user. - * @param {ServerRequest} req - The server request object. + * Delete prompt groups solely owned by the user and clean up their prompts/ACLs. + * Groups with other owners are left intact; the caller is responsible for + * removing the user's own ACL principal entries separately. + * + * Also handles legacy (pre-ACL) prompt groups that only have the author field set, + * ensuring they are not orphaned if the permission migration has not been run. * @param {string} userId - The ID of the user whose prompts and prompt groups are to be deleted. */ - deleteUserPrompts: async (req, userId) => { + deleteUserPrompts: async (userId) => { try { - const promptGroups = await getAllPromptGroups(req, { author: new ObjectId(userId) }); + const userObjectId = new ObjectId(userId); + const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.PROMPTGROUP); - if (promptGroups.length === 0) { + const authoredGroups = await PromptGroup.find({ author: userObjectId }).select('_id').lean(); + const authoredGroupIds = authoredGroups.map((g) => g._id); + + const migratedEntries = + authoredGroupIds.length > 0 + ? await AclEntry.find({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: { $in: authoredGroupIds }, + }) + .select('resourceId') + .lean() + : []; + const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString())); + const legacyGroupIds = authoredGroupIds.filter((id) => !migratedIds.has(id.toString())); + + const allGroupIdsToDelete = [...soleOwnedIds, ...legacyGroupIds]; + + if (allGroupIdsToDelete.length === 0) { return; } - const groupIds = promptGroups.map((group) => group._id); - - for (const groupId of groupIds) { - await removeGroupFromAllProjects(groupId); - } + await Promise.all(allGroupIdsToDelete.map((id) => removeGroupFromAllProjects(id))); await AclEntry.deleteMany({ resourceType: ResourceType.PROMPTGROUP, - resourceId: { $in: groupIds }, + resourceId: { $in: allGroupIdsToDelete }, }); - await PromptGroup.deleteMany({ author: new ObjectId(userId) }); - await Prompt.deleteMany({ author: new ObjectId(userId) }); + await PromptGroup.deleteMany({ _id: { $in: allGroupIdsToDelete } }); + await Prompt.deleteMany({ groupId: { $in: allGroupIdsToDelete } }); } catch (error) { logger.error('[deleteUserPrompts] General error:', error); } diff --git a/api/models/Prompt.spec.js b/api/models/Prompt.spec.js index e00a1a518c..a2063e6cfc 100644 --- a/api/models/Prompt.spec.js +++ b/api/models/Prompt.spec.js @@ -561,4 +561,231 @@ describe('Prompt ACL Permissions', () => { expect(prompt._id.toString()).toBe(legacyPrompt._id.toString()); }); }); + + describe('deleteUserPrompts', () => { + let deletingUser; + let otherUser; + let soleOwnedGroup; + let multiOwnedGroup; + let sharedGroup; + let soleOwnedPrompt; + let multiOwnedPrompt; + let sharedPrompt; + + beforeAll(async () => { + deletingUser = await User.create({ + name: 'Deleting User', + email: 'deleting@example.com', + role: SystemRoles.USER, + }); + otherUser = await User.create({ + name: 'Other User', + email: 'other@example.com', + role: SystemRoles.USER, + }); + + const soleProductionId = new ObjectId(); + soleOwnedGroup = await PromptGroup.create({ + name: 'Sole Owned Group', + author: deletingUser._id, + authorName: deletingUser.name, + productionId: soleProductionId, + }); + soleOwnedPrompt = await Prompt.create({ + prompt: 'Sole owned prompt', + author: deletingUser._id, + groupId: soleOwnedGroup._id, + type: 'text', + }); + await PromptGroup.updateOne( + { _id: soleOwnedGroup._id }, + { productionId: soleOwnedPrompt._id }, + ); + + const multiProductionId = new ObjectId(); + multiOwnedGroup = await PromptGroup.create({ + name: 'Multi Owned Group', + author: deletingUser._id, + authorName: deletingUser.name, + productionId: multiProductionId, + }); + multiOwnedPrompt = await Prompt.create({ + prompt: 'Multi owned prompt', + author: deletingUser._id, + groupId: multiOwnedGroup._id, + type: 'text', + }); + await PromptGroup.updateOne( + { _id: multiOwnedGroup._id }, + { productionId: multiOwnedPrompt._id }, + ); + + const sharedProductionId = new ObjectId(); + sharedGroup = await PromptGroup.create({ + name: 'Shared Group (other user owns)', + author: otherUser._id, + authorName: otherUser.name, + productionId: sharedProductionId, + }); + sharedPrompt = await Prompt.create({ + prompt: 'Shared prompt', + author: otherUser._id, + groupId: sharedGroup._id, + type: 'text', + }); + await PromptGroup.updateOne({ _id: sharedGroup._id }, { productionId: sharedPrompt._id }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: deletingUser._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: soleOwnedGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: deletingUser._id, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: deletingUser._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: multiOwnedGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: deletingUser._id, + }); + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUser._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: multiOwnedGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: otherUser._id, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUser._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: sharedGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: otherUser._id, + }); + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: deletingUser._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: sharedGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, + grantedBy: otherUser._id, + }); + + const globalProject = await Project.findOne({ name: 'Global' }); + await Project.updateOne( + { _id: globalProject._id }, + { + $addToSet: { + promptGroupIds: { + $each: [soleOwnedGroup._id, multiOwnedGroup._id, sharedGroup._id], + }, + }, + }, + ); + + await promptFns.deleteUserPrompts(deletingUser._id.toString()); + }); + + test('should delete solely-owned prompt groups and their prompts', async () => { + expect(await PromptGroup.findById(soleOwnedGroup._id)).toBeNull(); + expect(await Prompt.findById(soleOwnedPrompt._id)).toBeNull(); + }); + + test('should remove solely-owned groups from projects', async () => { + const globalProject = await Project.findOne({ name: 'Global' }); + const projectGroupIds = globalProject.promptGroupIds.map((id) => id.toString()); + expect(projectGroupIds).not.toContain(soleOwnedGroup._id.toString()); + }); + + test('should remove all ACL entries for solely-owned groups', async () => { + const aclEntries = await AclEntry.find({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: soleOwnedGroup._id, + }); + expect(aclEntries).toHaveLength(0); + }); + + test('should preserve multi-owned prompt groups', async () => { + expect(await PromptGroup.findById(multiOwnedGroup._id)).not.toBeNull(); + expect(await Prompt.findById(multiOwnedPrompt._id)).not.toBeNull(); + }); + + test('should preserve ACL entries of other owners on multi-owned groups', async () => { + const otherOwnerAcl = await AclEntry.findOne({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: multiOwnedGroup._id, + principalId: otherUser._id, + }); + expect(otherOwnerAcl).not.toBeNull(); + expect(otherOwnerAcl.permBits & PermissionBits.DELETE).toBeTruthy(); + }); + + test('should preserve groups owned by other users', async () => { + expect(await PromptGroup.findById(sharedGroup._id)).not.toBeNull(); + expect(await Prompt.findById(sharedPrompt._id)).not.toBeNull(); + }); + + test('should preserve project membership of non-deleted groups', async () => { + const globalProject = await Project.findOne({ name: 'Global' }); + const projectGroupIds = globalProject.promptGroupIds.map((id) => id.toString()); + expect(projectGroupIds).toContain(multiOwnedGroup._id.toString()); + expect(projectGroupIds).toContain(sharedGroup._id.toString()); + }); + + test('should preserve ACL entries for shared group owned by other user', async () => { + const ownerAcl = await AclEntry.findOne({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: sharedGroup._id, + principalId: otherUser._id, + }); + expect(ownerAcl).not.toBeNull(); + }); + + test('should be a no-op when user has no owned prompt groups', async () => { + const unrelatedUser = await User.create({ + name: 'Unrelated User', + email: 'unrelated@example.com', + role: SystemRoles.USER, + }); + + const beforeCount = await PromptGroup.countDocuments(); + await promptFns.deleteUserPrompts(unrelatedUser._id.toString()); + const afterCount = await PromptGroup.countDocuments(); + + expect(afterCount).toBe(beforeCount); + }); + + test('should delete legacy prompt groups that have author but no ACL entries', async () => { + const legacyUser = await User.create({ + name: 'Legacy User', + email: 'legacy-prompt@example.com', + role: SystemRoles.USER, + }); + + const legacyGroup = await PromptGroup.create({ + name: 'Legacy Group (no ACL)', + author: legacyUser._id, + authorName: legacyUser.name, + productionId: new ObjectId(), + }); + const legacyPrompt = await Prompt.create({ + prompt: 'Legacy prompt text', + author: legacyUser._id, + groupId: legacyGroup._id, + type: 'text', + }); + + await promptFns.deleteUserPrompts(legacyUser._id.toString()); + + expect(await PromptGroup.findById(legacyGroup._id)).toBeNull(); + expect(await Prompt.findById(legacyPrompt._id)).toBeNull(); + }); + }); }); diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 6d5df0ac8d..51f6d218ec 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -1,11 +1,18 @@ +const mongoose = require('mongoose'); const { logger, webSearchKeys } = require('@librechat/data-schemas'); -const { Tools, CacheKeys, Constants, FileSources } = require('librechat-data-provider'); const { MCPOAuthHandler, MCPTokenStorage, normalizeHttpError, extractWebSearchEnvVars, } = require('@librechat/api'); +const { + Tools, + CacheKeys, + Constants, + FileSources, + ResourceType, +} = require('librechat-data-provider'); const { deleteAllUserSessions, deleteAllSharedLinks, @@ -45,6 +52,7 @@ const { getAppConfig } = require('~/server/services/Config'); const { deleteToolCalls } = require('~/models/ToolCall'); const { deleteUserPrompts } = require('~/models/Prompt'); const { deleteUserAgents } = require('~/models/Agent'); +const { getSoleOwnedResourceIds } = require('~/server/services/PermissionService'); const { getLogStores } = require('~/cache'); const getUserController = async (req, res) => { @@ -113,6 +121,78 @@ const deleteUserFiles = async (req) => { } }; +/** + * Deletes MCP servers solely owned by the user and cleans up their ACLs. + * Disconnects live sessions for deleted servers before removing DB records. + * Servers with other owners are left intact; the caller is responsible for + * removing the user's own ACL principal entries separately. + * + * Also handles legacy (pre-ACL) MCP servers that only have the author field set, + * ensuring they are not orphaned if no permission migration has been run. + * @param {string} userId - The ID of the user. + */ +const deleteUserMcpServers = async (userId) => { + try { + const MCPServer = mongoose.models.MCPServer; + if (!MCPServer) { + return; + } + + const userObjectId = new mongoose.Types.ObjectId(userId); + const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.MCPSERVER); + + const authoredServers = await MCPServer.find({ author: userObjectId }) + .select('_id serverName') + .lean(); + + const migratedEntries = + authoredServers.length > 0 + ? await AclEntry.find({ + resourceType: ResourceType.MCPSERVER, + resourceId: { $in: authoredServers.map((s) => s._id) }, + }) + .select('resourceId') + .lean() + : []; + const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString())); + const legacyServers = authoredServers.filter((s) => !migratedIds.has(s._id.toString())); + const legacyServerIds = legacyServers.map((s) => s._id); + + const allServerIdsToDelete = [...soleOwnedIds, ...legacyServerIds]; + + if (allServerIdsToDelete.length === 0) { + return; + } + + const aclOwnedServers = + soleOwnedIds.length > 0 + ? await MCPServer.find({ _id: { $in: soleOwnedIds } }) + .select('serverName') + .lean() + : []; + const allServersToDelete = [...aclOwnedServers, ...legacyServers]; + + const mcpManager = getMCPManager(); + if (mcpManager) { + await Promise.all( + allServersToDelete.map(async (s) => { + await mcpManager.disconnectUserConnection(userId, s.serverName); + await invalidateCachedTools({ userId, serverName: s.serverName }); + }), + ); + } + + await AclEntry.deleteMany({ + resourceType: ResourceType.MCPSERVER, + resourceId: { $in: allServerIdsToDelete }, + }); + + await MCPServer.deleteMany({ _id: { $in: allServerIdsToDelete } }); + } catch (error) { + logger.error('[deleteUserMcpServers] General error:', error); + } +}; + const updateUserPluginsController = async (req, res) => { const appConfig = await getAppConfig({ role: req.user?.role }); const { user } = req; @@ -281,7 +361,8 @@ const deleteUserController = async (req, res) => { await Assistant.deleteMany({ user: user.id }); // delete user assistants await ConversationTag.deleteMany({ user: user.id }); // delete user conversation tags await MemoryEntry.deleteMany({ userId: user.id }); // delete user memory entries - await deleteUserPrompts(req, user.id); // delete user prompts + await deleteUserPrompts(user.id); // delete user prompts + await deleteUserMcpServers(user.id); // delete user MCP servers await Action.deleteMany({ user: user.id }); // delete user actions await Token.deleteMany({ userId: user.id }); // delete user OAuth tokens await Group.updateMany( @@ -439,4 +520,5 @@ module.exports = { verifyEmailController, updateUserPluginsController, resendVerificationController, + deleteUserMcpServers, }; diff --git a/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js index d0f54a046f..6382cd1d8e 100644 --- a/api/server/controllers/__tests__/deleteUser.spec.js +++ b/api/server/controllers/__tests__/deleteUser.spec.js @@ -104,6 +104,10 @@ jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn(), })); +jest.mock('~/server/services/PermissionService', () => ({ + getSoleOwnedResourceIds: jest.fn().mockResolvedValue([]), +})); + jest.mock('~/models/ToolCall', () => ({ deleteToolCalls: (...args) => mockDeleteToolCalls(...args), })); diff --git a/api/server/controllers/__tests__/deleteUserMcpServers.spec.js b/api/server/controllers/__tests__/deleteUserMcpServers.spec.js new file mode 100644 index 0000000000..fcb3211f24 --- /dev/null +++ b/api/server/controllers/__tests__/deleteUserMcpServers.spec.js @@ -0,0 +1,319 @@ +const mockGetMCPManager = jest.fn(); +const mockInvalidateCachedTools = jest.fn(); + +jest.mock('~/config', () => ({ + getMCPManager: (...args) => mockGetMCPManager(...args), + getFlowStateManager: jest.fn(), + getMCPServersRegistry: jest.fn(), +})); + +jest.mock('~/server/services/Config/getCachedTools', () => ({ + invalidateCachedTools: (...args) => mockInvalidateCachedTools(...args), +})); + +jest.mock('~/server/services/Config', () => ({ + getAppConfig: jest.fn(), + getMCPServerTools: jest.fn(), +})); + +const mongoose = require('mongoose'); +const { mcpServerSchema } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { + ResourceType, + AccessRoleIds, + PrincipalType, + PermissionBits, +} = require('librechat-data-provider'); +const permissionService = require('~/server/services/PermissionService'); +const { deleteUserMcpServers } = require('~/server/controllers/UserController'); +const { AclEntry, AccessRole } = require('~/db/models'); + +let MCPServer; + +describe('deleteUserMcpServers', () => { + let mongoServer; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + MCPServer = mongoose.models.MCPServer || mongoose.model('MCPServer', mcpServerSchema); + await mongoose.connect(mongoUri); + + await AccessRole.create({ + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + name: 'MCP Server Owner', + resourceType: ResourceType.MCPSERVER, + permBits: + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, + }); + + await AccessRole.create({ + accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, + name: 'MCP Server Viewer', + resourceType: ResourceType.MCPSERVER, + permBits: PermissionBits.VIEW, + }); + }, 20000); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await MCPServer.deleteMany({}); + await AclEntry.deleteMany({}); + jest.clearAllMocks(); + }); + + test('should delete solely-owned MCP servers and their ACL entries', async () => { + const userId = new mongoose.Types.ObjectId(); + + const server = await MCPServer.create({ + serverName: 'sole-owned-server', + config: { title: 'Test Server' }, + author: userId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.MCPSERVER, + resourceId: server._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: userId, + }); + + mockGetMCPManager.mockReturnValue({ + disconnectUserConnection: jest.fn().mockResolvedValue(undefined), + }); + + await deleteUserMcpServers(userId.toString()); + + expect(await MCPServer.findById(server._id)).toBeNull(); + + const aclEntries = await AclEntry.find({ + resourceType: ResourceType.MCPSERVER, + resourceId: server._id, + }); + expect(aclEntries).toHaveLength(0); + }); + + test('should disconnect MCP sessions and invalidate tool cache before deletion', async () => { + const userId = new mongoose.Types.ObjectId(); + const mockDisconnect = jest.fn().mockResolvedValue(undefined); + + const server = await MCPServer.create({ + serverName: 'session-server', + config: { title: 'Session Server' }, + author: userId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.MCPSERVER, + resourceId: server._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: userId, + }); + + mockGetMCPManager.mockReturnValue({ disconnectUserConnection: mockDisconnect }); + + await deleteUserMcpServers(userId.toString()); + + expect(mockDisconnect).toHaveBeenCalledWith(userId.toString(), 'session-server'); + expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ + userId: userId.toString(), + serverName: 'session-server', + }); + }); + + test('should preserve multi-owned MCP servers', async () => { + const deletingUserId = new mongoose.Types.ObjectId(); + const otherOwnerId = new mongoose.Types.ObjectId(); + + const soleServer = await MCPServer.create({ + serverName: 'sole-server', + config: { title: 'Sole Server' }, + author: deletingUserId, + }); + + const multiServer = await MCPServer.create({ + serverName: 'multi-server', + config: { title: 'Multi Server' }, + author: deletingUserId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: deletingUserId, + resourceType: ResourceType.MCPSERVER, + resourceId: soleServer._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: deletingUserId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: deletingUserId, + resourceType: ResourceType.MCPSERVER, + resourceId: multiServer._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: deletingUserId, + }); + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: otherOwnerId, + resourceType: ResourceType.MCPSERVER, + resourceId: multiServer._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: otherOwnerId, + }); + + mockGetMCPManager.mockReturnValue({ + disconnectUserConnection: jest.fn().mockResolvedValue(undefined), + }); + + await deleteUserMcpServers(deletingUserId.toString()); + + expect(await MCPServer.findById(soleServer._id)).toBeNull(); + expect(await MCPServer.findById(multiServer._id)).not.toBeNull(); + + const soleAcl = await AclEntry.find({ + resourceType: ResourceType.MCPSERVER, + resourceId: soleServer._id, + }); + expect(soleAcl).toHaveLength(0); + + const multiAclOther = await AclEntry.find({ + resourceType: ResourceType.MCPSERVER, + resourceId: multiServer._id, + principalId: otherOwnerId, + }); + expect(multiAclOther).toHaveLength(1); + expect(multiAclOther[0].permBits & PermissionBits.DELETE).toBeTruthy(); + + const multiAclDeleting = await AclEntry.find({ + resourceType: ResourceType.MCPSERVER, + resourceId: multiServer._id, + principalId: deletingUserId, + }); + expect(multiAclDeleting).toHaveLength(1); + }); + + test('should be a no-op when user has no owned MCP servers', async () => { + const userId = new mongoose.Types.ObjectId(); + + const otherUserId = new mongoose.Types.ObjectId(); + const server = await MCPServer.create({ + serverName: 'other-server', + config: { title: 'Other Server' }, + author: otherUserId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.MCPSERVER, + resourceId: server._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: otherUserId, + }); + + await deleteUserMcpServers(userId.toString()); + + expect(await MCPServer.findById(server._id)).not.toBeNull(); + expect(mockGetMCPManager).not.toHaveBeenCalled(); + }); + + test('should handle gracefully when MCPServer model is not registered', async () => { + const originalModel = mongoose.models.MCPServer; + delete mongoose.models.MCPServer; + + try { + const userId = new mongoose.Types.ObjectId(); + await expect(deleteUserMcpServers(userId.toString())).resolves.toBeUndefined(); + } finally { + mongoose.models.MCPServer = originalModel; + } + }); + + test('should handle gracefully when MCPManager is not available', async () => { + const userId = new mongoose.Types.ObjectId(); + + const server = await MCPServer.create({ + serverName: 'no-manager-server', + config: { title: 'No Manager Server' }, + author: userId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.MCPSERVER, + resourceId: server._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: userId, + }); + + mockGetMCPManager.mockReturnValue(null); + + await deleteUserMcpServers(userId.toString()); + + expect(await MCPServer.findById(server._id)).toBeNull(); + }); + + test('should delete legacy MCP servers that have author but no ACL entries', async () => { + const legacyUserId = new mongoose.Types.ObjectId(); + + const legacyServer = await MCPServer.create({ + serverName: 'legacy-server', + config: { title: 'Legacy Server' }, + author: legacyUserId, + }); + + mockGetMCPManager.mockReturnValue({ + disconnectUserConnection: jest.fn().mockResolvedValue(undefined), + }); + + await deleteUserMcpServers(legacyUserId.toString()); + + expect(await MCPServer.findById(legacyServer._id)).toBeNull(); + }); + + test('should delete both ACL-owned and legacy servers in one call', async () => { + const userId = new mongoose.Types.ObjectId(); + + const aclServer = await MCPServer.create({ + serverName: 'acl-server', + config: { title: 'ACL Server' }, + author: userId, + }); + + await permissionService.grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.MCPSERVER, + resourceId: aclServer._id, + accessRoleId: AccessRoleIds.MCPSERVER_OWNER, + grantedBy: userId, + }); + + const legacyServer = await MCPServer.create({ + serverName: 'legacy-mixed-server', + config: { title: 'Legacy Mixed' }, + author: userId, + }); + + mockGetMCPManager.mockReturnValue({ + disconnectUserConnection: jest.fn().mockResolvedValue(undefined), + }); + + await deleteUserMcpServers(userId.toString()); + + expect(await MCPServer.findById(aclServer._id)).toBeNull(); + expect(await MCPServer.findById(legacyServer._id)).toBeNull(); + }); +}); diff --git a/api/server/controllers/__tests__/deleteUserResourceCoverage.spec.js b/api/server/controllers/__tests__/deleteUserResourceCoverage.spec.js new file mode 100644 index 0000000000..b08e502800 --- /dev/null +++ b/api/server/controllers/__tests__/deleteUserResourceCoverage.spec.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const path = require('path'); +const { ResourceType } = require('librechat-data-provider'); + +/** + * Maps each ResourceType to the cleanup function name that must appear in + * deleteUserController's source to prove it is handled during user deletion. + * + * When a new ResourceType is added, this test will fail until a corresponding + * entry is added here (or to NO_USER_CLEANUP_NEEDED) AND the actual cleanup + * logic is implemented. + */ +const HANDLED_RESOURCE_TYPES = { + [ResourceType.AGENT]: 'deleteUserAgents', + [ResourceType.REMOTE_AGENT]: 'deleteUserAgents', + [ResourceType.PROMPTGROUP]: 'deleteUserPrompts', + [ResourceType.MCPSERVER]: 'deleteUserMcpServers', +}; + +/** + * ResourceTypes that are ACL-tracked but have no per-user deletion semantics + * (e.g., system resources, public-only). Must be explicitly listed here with + * a justification to prevent silent omissions. + */ +const NO_USER_CLEANUP_NEEDED = new Set([ + // Example: ResourceType.SYSTEM_TEMPLATE — public/system; not user-owned +]); + +describe('deleteUserController - resource type coverage guard', () => { + let controllerSource; + + beforeAll(() => { + controllerSource = fs.readFileSync(path.resolve(__dirname, '../UserController.js'), 'utf-8'); + }); + + test('every ResourceType must have a documented cleanup handler or explicit exclusion', () => { + const allTypes = Object.values(ResourceType); + const handledTypes = Object.keys(HANDLED_RESOURCE_TYPES); + const unhandledTypes = allTypes.filter( + (t) => !handledTypes.includes(t) && !NO_USER_CLEANUP_NEEDED.has(t), + ); + + expect(unhandledTypes).toEqual([]); + }); + + test('every cleanup handler referenced in HANDLED_RESOURCE_TYPES must appear in the controller source', () => { + const uniqueHandlers = [...new Set(Object.values(HANDLED_RESOURCE_TYPES))]; + + for (const handler of uniqueHandlers) { + expect(controllerSource).toContain(handler); + } + }); +}); diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index a843f48f6f..ba1ef68032 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -1,7 +1,12 @@ const mongoose = require('mongoose'); const { isEnabled } = require('@librechat/api'); const { getTransactionSupport, logger } = require('@librechat/data-schemas'); -const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider'); +const { + ResourceType, + PrincipalType, + PrincipalModel, + PermissionBits, +} = require('librechat-data-provider'); const { entraIdPrincipalFeatureEnabled, getUserOwnedEntraGroups, @@ -799,6 +804,49 @@ const bulkUpdateResourcePermissions = async ({ } }; +/** + * Returns resource IDs where the given user is the sole owner + * (no other principal holds the DELETE bit on the same resource). + * @param {mongoose.Types.ObjectId} userObjectId + * @param {string|string[]} resourceTypes - One or more ResourceType values. + * @returns {Promise} + */ +const getSoleOwnedResourceIds = async (userObjectId, resourceTypes) => { + const types = Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes]; + const ownedEntries = await AclEntry.find({ + principalType: PrincipalType.USER, + principalId: userObjectId, + resourceType: { $in: types }, + permBits: { $bitsAllSet: PermissionBits.DELETE }, + }) + .select('resourceId') + .lean(); + + if (ownedEntries.length === 0) { + return []; + } + + const ownedIds = ownedEntries.map((e) => e.resourceId); + + const otherOwners = await AclEntry.aggregate([ + { + $match: { + resourceType: { $in: types }, + resourceId: { $in: ownedIds }, + permBits: { $bitsAllSet: PermissionBits.DELETE }, + $or: [ + { principalId: { $ne: userObjectId } }, + { principalType: { $ne: PrincipalType.USER } }, + ], + }, + }, + { $group: { _id: '$resourceId' } }, + ]); + + const multiOwnerIds = new Set(otherOwners.map((doc) => doc._id.toString())); + return ownedIds.filter((id) => !multiOwnerIds.has(id.toString())); +}; + /** * Remove all permissions for a resource (cleanup when resource is deleted) * @param {Object} params - Parameters for removing all permissions @@ -839,5 +887,6 @@ module.exports = { ensurePrincipalExists, ensureGroupPrincipalExists, syncUserEntraGroupMemberships, + getSoleOwnedResourceIds, removeAllPermissions, }; From 748fd086c1f7d943267d35cc5df2334cba13e26b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 18:09:23 -0400 Subject: [PATCH 076/111] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Update=20`fast-?= =?UTF-8?q?xml-parser`=20to=20v5.5.7=20(#12317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump fast-xml-parser dependency from 5.5.6 to 5.5.7 for improved functionality and compatibility. - Update corresponding entries in both package.json and package-lock.json to reflect the new version. --- package-lock.json | 8 ++++---- package.json | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b0c0e4888..35aacac9c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27093,9 +27093,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", - "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.7.tgz", + "integrity": "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==", "funding": [ { "type": "github", @@ -27106,7 +27106,7 @@ "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.1.3", - "strnum": "^2.1.2" + "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" diff --git a/package.json b/package.json index e59032c7dd..de6a580a1a 100644 --- a/package.json +++ b/package.json @@ -139,13 +139,13 @@ "@librechat/agents": { "@langchain/anthropic": { "@anthropic-ai/sdk": "0.73.0", - "fast-xml-parser": "5.5.6" + "fast-xml-parser": "5.5.7" }, "@anthropic-ai/sdk": "0.73.0", - "fast-xml-parser": "5.5.6" + "fast-xml-parser": "5.5.7" }, "elliptic": "^6.6.1", - "fast-xml-parser": "5.5.6", + "fast-xml-parser": "5.5.7", "form-data": "^4.0.4", "tslib": "^2.8.1", "mdast-util-gfm-autolink-literal": "2.0.0", From ecd6d76bc84d4960f3969fab0a056cd2f6e5a5bf Mon Sep 17 00:00:00 2001 From: Brad Russell Date: Thu, 19 Mar 2026 21:48:03 -0400 Subject: [PATCH 077/111] =?UTF-8?q?=F0=9F=9A=A6=20fix:=20ERR=5FERL=5FINVAL?= =?UTF-8?q?ID=5FIP=5FADDRESS=20and=20IPv6=20Key=20Collisions=20in=20IP=20R?= =?UTF-8?q?ate=20Limiters=20(#12319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Add removePorts keyGenerator to all IP-based rate limiters Six IP-based rate limiters are missing the `keyGenerator: removePorts` option that is already used by the auth-related limiters (login, register, resetPassword, verifyEmail). Without it, reverse proxies that include ports in X-Forwarded-For headers cause ERR_ERL_INVALID_IP_ADDRESS errors from express-rate-limit. Fixes #12318 * fix: make removePorts IPv6-safe to prevent rate-limit key collisions The original regex `/:\d+[^:]*$/` treated the last colon-delimited segment of bare IPv6 addresses as a port, mangling valid IPs (e.g. `::1` → `::`, `2001:db8::1` → `2001:db8::`). Distinct IPv6 clients could collapse into the same rate-limit bucket. Use `net.isIP()` as a fast path for already-valid IPs, then match bracketed IPv6+port and IPv4+port explicitly. Bare IPv6 addresses are now returned unchanged. Also fixes pre-existing property ordering inconsistency in ttsLimiters.js userLimiterOptions (keyGenerator before store). * refactor: move removePorts to packages/api as TypeScript, fix import order - Move removePorts implementation to packages/api/src/utils/removePorts.ts with proper Express Request typing - Reduce api/server/utils/removePorts.js to a thin re-export from @librechat/api for backward compatibility - Consolidate removePorts import with limiterCache from @librechat/api in all 6 limiter files, fixing import order (package imports shortest to longest, local imports longest to shortest) - Remove narrating inline comments per code style guidelines --------- Co-authored-by: Danny Avila --- api/cache/banViolation.js | 3 +- api/server/middleware/checkBan.js | 5 +- .../middleware/limiters/forkLimiters.js | 3 +- .../middleware/limiters/importLimiters.js | 5 +- .../middleware/limiters/loginLimiter.js | 3 +- .../middleware/limiters/messageLimiters.js | 5 +- .../middleware/limiters/registerLimiter.js | 3 +- .../limiters/resetPasswordLimiter.js | 3 +- api/server/middleware/limiters/sttLimiters.js | 5 +- api/server/middleware/limiters/ttsLimiters.js | 7 +- .../middleware/limiters/uploadLimiters.js | 5 +- .../middleware/limiters/verifyEmailLimiter.js | 3 +- api/server/utils/index.js | 2 - api/server/utils/removePorts.js | 1 - packages/api/src/utils/index.ts | 1 + packages/api/src/utils/ports.spec.ts | 98 +++++++++++++++++++ packages/api/src/utils/ports.ts | 38 +++++++ 17 files changed, 162 insertions(+), 28 deletions(-) delete mode 100644 api/server/utils/removePorts.js create mode 100644 packages/api/src/utils/ports.spec.ts create mode 100644 packages/api/src/utils/ports.ts diff --git a/api/cache/banViolation.js b/api/cache/banViolation.js index 4d321889c1..36945ca420 100644 --- a/api/cache/banViolation.js +++ b/api/cache/banViolation.js @@ -1,8 +1,7 @@ const { logger } = require('@librechat/data-schemas'); -const { isEnabled, math } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); +const { isEnabled, math, removePorts } = require('@librechat/api'); const { deleteAllUserSessions } = require('~/models'); -const { removePorts } = require('~/server/utils'); const getLogStores = require('./getLogStores'); const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {}; diff --git a/api/server/middleware/checkBan.js b/api/server/middleware/checkBan.js index 79804a84e1..0c98f3a824 100644 --- a/api/server/middleware/checkBan.js +++ b/api/server/middleware/checkBan.js @@ -1,11 +1,10 @@ const { Keyv } = require('keyv'); const uap = require('ua-parser-js'); const { logger } = require('@librechat/data-schemas'); -const { isEnabled, keyvMongo } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); -const { removePorts } = require('~/server/utils'); -const denyRequest = require('./denyRequest'); +const { isEnabled, keyvMongo, removePorts } = require('@librechat/api'); const { getLogStores } = require('~/cache'); +const denyRequest = require('./denyRequest'); const { findUser } = require('~/models'); const banCache = new Keyv({ store: keyvMongo, namespace: ViolationTypes.BAN, ttl: 0 }); diff --git a/api/server/middleware/limiters/forkLimiters.js b/api/server/middleware/limiters/forkLimiters.js index f1e9b15f11..6d05cedad5 100644 --- a/api/server/middleware/limiters/forkLimiters.js +++ b/api/server/middleware/limiters/forkLimiters.js @@ -1,6 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); +const { limiterCache, removePorts } = require('@librechat/api'); const logViolation = require('~/cache/logViolation'); const getEnvironmentVariables = () => { @@ -59,6 +59,7 @@ const createForkLimiters = () => { windowMs: forkIpWindowMs, max: forkIpMax, handler: createForkHandler(), + keyGenerator: removePorts, store: limiterCache('fork_ip_limiter'), }; const userLimiterOptions = { diff --git a/api/server/middleware/limiters/importLimiters.js b/api/server/middleware/limiters/importLimiters.js index f383e99563..22b7013558 100644 --- a/api/server/middleware/limiters/importLimiters.js +++ b/api/server/middleware/limiters/importLimiters.js @@ -1,6 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); +const { limiterCache, removePorts } = require('@librechat/api'); const logViolation = require('~/cache/logViolation'); const getEnvironmentVariables = () => { @@ -60,6 +60,7 @@ const createImportLimiters = () => { windowMs: importIpWindowMs, max: importIpMax, handler: createImportHandler(), + keyGenerator: removePorts, store: limiterCache('import_ip_limiter'), }; const userLimiterOptions = { @@ -67,7 +68,7 @@ const createImportLimiters = () => { max: importUserMax, handler: createImportHandler(false), keyGenerator: function (req) { - return req.user?.id; // Use the user ID or NULL if not available + return req.user?.id; }, store: limiterCache('import_user_limiter'), }; diff --git a/api/server/middleware/limiters/loginLimiter.js b/api/server/middleware/limiters/loginLimiter.js index eef0c56bfc..c178b68a25 100644 --- a/api/server/middleware/limiters/loginLimiter.js +++ b/api/server/middleware/limiters/loginLimiter.js @@ -1,7 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); -const { removePorts } = require('~/server/utils'); +const { limiterCache, removePorts } = require('@librechat/api'); const { logViolation } = require('~/cache'); const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env; diff --git a/api/server/middleware/limiters/messageLimiters.js b/api/server/middleware/limiters/messageLimiters.js index 50f4dbc644..4f1d72076f 100644 --- a/api/server/middleware/limiters/messageLimiters.js +++ b/api/server/middleware/limiters/messageLimiters.js @@ -1,6 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); +const { limiterCache, removePorts } = require('@librechat/api'); const denyRequest = require('~/server/middleware/denyRequest'); const { logViolation } = require('~/cache'); @@ -50,6 +50,7 @@ const ipLimiterOptions = { windowMs: ipWindowMs, max: ipMax, handler: createHandler(), + keyGenerator: removePorts, store: limiterCache('message_ip_limiter'), }; @@ -58,7 +59,7 @@ const userLimiterOptions = { max: userMax, handler: createHandler(false), keyGenerator: function (req) { - return req.user?.id; // Use the user ID or NULL if not available + return req.user?.id; }, store: limiterCache('message_user_limiter'), }; diff --git a/api/server/middleware/limiters/registerLimiter.js b/api/server/middleware/limiters/registerLimiter.js index eeebebdb42..91ea027376 100644 --- a/api/server/middleware/limiters/registerLimiter.js +++ b/api/server/middleware/limiters/registerLimiter.js @@ -1,7 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); -const { removePorts } = require('~/server/utils'); +const { limiterCache, removePorts } = require('@librechat/api'); const { logViolation } = require('~/cache'); const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env; diff --git a/api/server/middleware/limiters/resetPasswordLimiter.js b/api/server/middleware/limiters/resetPasswordLimiter.js index d1dfe52a98..7feca47ca5 100644 --- a/api/server/middleware/limiters/resetPasswordLimiter.js +++ b/api/server/middleware/limiters/resetPasswordLimiter.js @@ -1,7 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); -const { removePorts } = require('~/server/utils'); +const { limiterCache, removePorts } = require('@librechat/api'); const { logViolation } = require('~/cache'); const { diff --git a/api/server/middleware/limiters/sttLimiters.js b/api/server/middleware/limiters/sttLimiters.js index f2f47cf680..ded9040033 100644 --- a/api/server/middleware/limiters/sttLimiters.js +++ b/api/server/middleware/limiters/sttLimiters.js @@ -1,6 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); +const { limiterCache, removePorts } = require('@librechat/api'); const logViolation = require('~/cache/logViolation'); const getEnvironmentVariables = () => { @@ -54,6 +54,7 @@ const createSTTLimiters = () => { windowMs: sttIpWindowMs, max: sttIpMax, handler: createSTTHandler(), + keyGenerator: removePorts, store: limiterCache('stt_ip_limiter'), }; @@ -62,7 +63,7 @@ const createSTTLimiters = () => { max: sttUserMax, handler: createSTTHandler(false), keyGenerator: function (req) { - return req.user?.id; // Use the user ID or NULL if not available + return req.user?.id; }, store: limiterCache('stt_user_limiter'), }; diff --git a/api/server/middleware/limiters/ttsLimiters.js b/api/server/middleware/limiters/ttsLimiters.js index 41dd9a6ba5..7ded475230 100644 --- a/api/server/middleware/limiters/ttsLimiters.js +++ b/api/server/middleware/limiters/ttsLimiters.js @@ -1,6 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); +const { limiterCache, removePorts } = require('@librechat/api'); const logViolation = require('~/cache/logViolation'); const getEnvironmentVariables = () => { @@ -54,6 +54,7 @@ const createTTSLimiters = () => { windowMs: ttsIpWindowMs, max: ttsIpMax, handler: createTTSHandler(), + keyGenerator: removePorts, store: limiterCache('tts_ip_limiter'), }; @@ -61,10 +62,10 @@ const createTTSLimiters = () => { windowMs: ttsUserWindowMs, max: ttsUserMax, handler: createTTSHandler(false), - store: limiterCache('tts_user_limiter'), keyGenerator: function (req) { - return req.user?.id; // Use the user ID or NULL if not available + return req.user?.id; }, + store: limiterCache('tts_user_limiter'), }; const ttsIpLimiter = rateLimit(ipLimiterOptions); diff --git a/api/server/middleware/limiters/uploadLimiters.js b/api/server/middleware/limiters/uploadLimiters.js index df6987877c..8c878cfa86 100644 --- a/api/server/middleware/limiters/uploadLimiters.js +++ b/api/server/middleware/limiters/uploadLimiters.js @@ -1,6 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); +const { limiterCache, removePorts } = require('@librechat/api'); const logViolation = require('~/cache/logViolation'); const getEnvironmentVariables = () => { @@ -60,6 +60,7 @@ const createFileLimiters = () => { windowMs: fileUploadIpWindowMs, max: fileUploadIpMax, handler: createFileUploadHandler(), + keyGenerator: removePorts, store: limiterCache('file_upload_ip_limiter'), }; @@ -68,7 +69,7 @@ const createFileLimiters = () => { max: fileUploadUserMax, handler: createFileUploadHandler(false), keyGenerator: function (req) { - return req.user?.id; // Use the user ID or NULL if not available + return req.user?.id; }, store: limiterCache('file_upload_user_limiter'), }; diff --git a/api/server/middleware/limiters/verifyEmailLimiter.js b/api/server/middleware/limiters/verifyEmailLimiter.js index 006c4df656..5844686bf0 100644 --- a/api/server/middleware/limiters/verifyEmailLimiter.js +++ b/api/server/middleware/limiters/verifyEmailLimiter.js @@ -1,7 +1,6 @@ const rateLimit = require('express-rate-limit'); -const { limiterCache } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); -const { removePorts } = require('~/server/utils'); +const { limiterCache, removePorts } = require('@librechat/api'); const { logViolation } = require('~/cache'); const { diff --git a/api/server/utils/index.js b/api/server/utils/index.js index 918ab54f85..59cb71625f 100644 --- a/api/server/utils/index.js +++ b/api/server/utils/index.js @@ -1,4 +1,3 @@ -const removePorts = require('./removePorts'); const handleText = require('./handleText'); const sendEmail = require('./sendEmail'); const queue = require('./queue'); @@ -6,7 +5,6 @@ const files = require('./files'); module.exports = { ...handleText, - removePorts, sendEmail, ...files, ...queue, diff --git a/api/server/utils/removePorts.js b/api/server/utils/removePorts.js deleted file mode 100644 index 375ff1cc71..0000000000 --- a/api/server/utils/removePorts.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = (req) => req?.ip?.replace(/:\d+[^:]*$/, ''); diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index a1412e21f2..3320fef949 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -18,6 +18,7 @@ export * from './math'; export * from './oidc'; export * from './openid'; export * from './promise'; +export * from './ports'; export * from './sanitizeTitle'; export * from './tempChatRetention'; export * from './text'; diff --git a/packages/api/src/utils/ports.spec.ts b/packages/api/src/utils/ports.spec.ts new file mode 100644 index 0000000000..ea4dc284c7 --- /dev/null +++ b/packages/api/src/utils/ports.spec.ts @@ -0,0 +1,98 @@ +import type { Request } from 'express'; +import { removePorts } from './ports'; + +const req = (ip: string | undefined): Request => ({ ip }) as Request; + +describe('removePorts', () => { + describe('bare IPv4 (no port)', () => { + test('returns a standard private IP unchanged', () => { + expect(removePorts(req('192.168.1.1'))).toBe('192.168.1.1'); + }); + + test('returns a public IP unchanged', () => { + expect(removePorts(req('149.154.20.46'))).toBe('149.154.20.46'); + }); + + test('returns loopback unchanged', () => { + expect(removePorts(req('127.0.0.1'))).toBe('127.0.0.1'); + }); + }); + + describe('IPv4 with port (the primary bug scenario)', () => { + test('strips port from a private IP', () => { + expect(removePorts(req('192.168.1.1:8080'))).toBe('192.168.1.1'); + }); + + test('strips port from the IP in the original issue report', () => { + expect(removePorts(req('149.154.20.46:48198'))).toBe('149.154.20.46'); + }); + + test('strips a low port number', () => { + expect(removePorts(req('10.0.0.1:80'))).toBe('10.0.0.1'); + }); + + test('strips a high port number', () => { + expect(removePorts(req('10.0.0.1:65535'))).toBe('10.0.0.1'); + }); + }); + + describe('bare IPv6 (no port)', () => { + test('returns loopback unchanged', () => { + expect(removePorts(req('::1'))).toBe('::1'); + }); + + test('returns a full address unchanged', () => { + expect(removePorts(req('2001:db8::1'))).toBe('2001:db8::1'); + }); + + test('returns an IPv4-mapped IPv6 address unchanged', () => { + expect(removePorts(req('::ffff:192.168.1.1'))).toBe('::ffff:192.168.1.1'); + }); + + test('returns a fully expanded IPv6 unchanged', () => { + expect(removePorts(req('2001:0db8:85a3:0000:0000:8a2e:0370:7334'))).toBe( + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + ); + }); + }); + + describe('bracketed IPv6 with port', () => { + test('extracts loopback from brackets with port', () => { + expect(removePorts(req('[::1]:8080'))).toBe('::1'); + }); + + test('extracts a full address from brackets with port', () => { + expect(removePorts(req('[2001:db8::1]:443'))).toBe('2001:db8::1'); + }); + + test('extracts address from brackets without port', () => { + expect(removePorts(req('[::1]'))).toBe('::1'); + }); + }); + + describe('falsy / missing ip', () => { + test('returns undefined when ip is undefined', () => { + expect(removePorts(req(undefined))).toBeUndefined(); + }); + + test('returns undefined when ip is empty string', () => { + expect(removePorts({ ip: '' } as Request)).toBe(''); + }); + + test('returns undefined when req is null', () => { + expect(removePorts(null as unknown as Request)).toBeUndefined(); + }); + }); + + describe('IPv4-mapped IPv6 with port', () => { + test('strips port from an IPv4-mapped IPv6 address', () => { + expect(removePorts(req('::ffff:1.2.3.4:8080'))).toBe('::ffff:1.2.3.4'); + }); + }); + + describe('unrecognized formats fall through unchanged', () => { + test('returns garbage input unchanged', () => { + expect(removePorts(req('not-an-ip'))).toBe('not-an-ip'); + }); + }); +}); diff --git a/packages/api/src/utils/ports.ts b/packages/api/src/utils/ports.ts new file mode 100644 index 0000000000..ecd38039d6 --- /dev/null +++ b/packages/api/src/utils/ports.ts @@ -0,0 +1,38 @@ +import type { Request } from 'express'; + +/** Strips port suffix from req.ip for use as a rate-limiter key (IPv4 and IPv6-safe) */ +export function removePorts(req: Request): string | undefined { + const ip = req?.ip; + if (!ip) { + return ip; + } + + if (ip.charCodeAt(0) === 91) { + const close = ip.indexOf(']'); + return close > 0 ? ip.slice(1, close) : ip; + } + + const lastColon = ip.lastIndexOf(':'); + if (lastColon === -1) { + return ip; + } + + if (ip.indexOf('.') !== -1 && hasOnlyDigitsAfter(ip, lastColon + 1)) { + return ip.slice(0, lastColon); + } + + return ip; +} + +function hasOnlyDigitsAfter(str: string, start: number): boolean { + if (start >= str.length) { + return false; + } + for (let i = start; i < str.length; i++) { + const c = str.charCodeAt(i); + if (c < 48 || c > 57) { + return false; + } + } + return true; +} From e442984364db02163f3cc3ecb7b2ee5efba66fb9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 19 Mar 2026 22:13:40 -0400 Subject: [PATCH 078/111] =?UTF-8?q?=F0=9F=92=A3=20fix:=20Harden=20against?= =?UTF-8?q?=20falsified=20ZIP=20metadata=20in=20ODT=20parsing=20(#12320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * security: replace JSZip metadata guard with yauzl streaming decompression The ODT decompressed-size guard was checking JSZip's private _data.uncompressedSize fields, which are populated from the ZIP central directory — attacker-controlled metadata. A crafted ODT with falsified uncompressedSize values bypassed the 50MB cap entirely, allowing content.xml decompression to exhaust Node.js heap memory (DoS). Replace JSZip with yauzl for ODT extraction. The new extractOdtContentXml function uses yauzl's streaming API: it lazily iterates ZIP entries, opens a decompression stream for content.xml, and counts real bytes as they arrive from the inflate stream. The stream is destroyed the moment the byte count crosses ODT_MAX_DECOMPRESSED_SIZE, aborting the inflate before the full payload is materialised in memory. - Remove jszip from direct dependencies (still transitive via mammoth) - Add yauzl + @types/yauzl - Update zip-bomb test to verify streaming abort with DEFLATE payload * fix: close file descriptor leaks and declare jszip test dependency - Use a shared `finish()` helper in extractOdtContentXml that calls zipfile.close() on every exit path (success, size cap, missing entry, openReadStream errors, zipfile errors). Without this, any error path leaked one OS file descriptor permanently — uploading many malformed ODTs could exhaust the process FD limit (a distinct DoS vector). - Add jszip to devDependencies so the zip-bomb test has an explicit dependency rather than relying on mammoth's transitive jszip. - Update JSDoc to document that all exit paths close the zipfile. * fix: move yauzl from dependencies to peerDependencies Matches the established pattern for runtime parser libraries in packages/api: mammoth, pdfjs-dist, and xlsx are all peerDependencies (provided by the consuming /api workspace) with devDependencies for testing. yauzl was incorrectly placed in dependencies. * fix: add yauzl to /api dependencies to satisfy peer dep packages/api declares yauzl as a peerDependency; /api is the consuming workspace that must provide it at runtime, matching the pattern used for mammoth, pdfjs-dist, and xlsx. --- api/package.json | 1 + package-lock.json | 23 ++-- packages/api/package.json | 9 +- packages/api/src/files/documents/crud.spec.ts | 4 +- packages/api/src/files/documents/crud.ts | 107 ++++++++++++++---- 5 files changed, 108 insertions(+), 36 deletions(-) diff --git a/api/package.json b/api/package.json index 2255679dae..4416acd1d8 100644 --- a/api/package.json +++ b/api/package.json @@ -113,6 +113,7 @@ "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "yauzl": "^3.2.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index 35aacac9c2..5d8264f602 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,6 +128,7 @@ "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "yauzl": "^3.2.1", "zod": "^3.22.4" }, "devDependencies": { @@ -21269,6 +21270,16 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.24.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", @@ -23080,7 +23091,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -35393,7 +35403,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -43740,7 +43749,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz", "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==", - "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", @@ -43802,9 +43810,6 @@ "name": "@librechat/api", "version": "1.7.26", "license": "ISC", - "dependencies": { - "jszip": "^3.10.1" - }, "devDependencies": { "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", @@ -43825,8 +43830,10 @@ "@types/node-fetch": "^2.6.13", "@types/react": "^18.2.18", "@types/winston": "^2.4.4", + "@types/yauzl": "^2.10.3", "jest": "^30.2.0", "jest-junit": "^16.0.0", + "jszip": "^3.10.1", "librechat-data-provider": "*", "mammoth": "^1.11.0", "mongodb": "^6.14.2", @@ -43836,7 +43843,8 @@ "rollup-plugin-peer-deps-external": "^2.2.4", "ts-node": "^10.9.2", "typescript": "^5.0.4", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "yauzl": "^3.2.1" }, "peerDependencies": { "@anthropic-ai/vertex-sdk": "^0.14.3", @@ -43876,6 +43884,7 @@ "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "undici": "^7.24.1", + "yauzl": "^3.2.1", "zod": "^3.22.4" } }, diff --git a/packages/api/package.json b/packages/api/package.json index 57675ee371..3a3b3caef6 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -64,7 +64,9 @@ "@types/node-fetch": "^2.6.13", "@types/react": "^18.2.18", "@types/winston": "^2.4.4", + "@types/yauzl": "^2.10.3", "jest": "^30.2.0", + "jszip": "^3.10.1", "jest-junit": "^16.0.0", "librechat-data-provider": "*", "mammoth": "^1.11.0", @@ -75,7 +77,8 @@ "rollup-plugin-peer-deps-external": "^2.2.4", "ts-node": "^10.9.2", "typescript": "^5.0.4", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "yauzl": "^3.2.1" }, "publishConfig": { "registry": "https://registry.npmjs.org/" @@ -118,9 +121,7 @@ "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "undici": "^7.24.1", + "yauzl": "^3.2.1", "zod": "^3.22.4" - }, - "dependencies": { - "jszip": "^3.10.1" } } diff --git a/packages/api/src/files/documents/crud.spec.ts b/packages/api/src/files/documents/crud.spec.ts index a1c317279c..2a5086869f 100644 --- a/packages/api/src/files/documents/crud.spec.ts +++ b/packages/api/src/files/documents/crud.spec.ts @@ -104,7 +104,7 @@ describe('Document Parser', () => { await expect(parseDocument({ file })).rejects.toThrow('No text found in document'); }); - test('parseDocument() throws for odt whose decompressed content exceeds the size limit', async () => { + test('parseDocument() aborts decompression when content.xml exceeds the size limit', async () => { const zip = new JSZip(); zip.file('mimetype', 'application/vnd.oasis.opendocument.text', { compression: 'STORE' }); zip.file('content.xml', 'x'.repeat(51 * 1024 * 1024), { compression: 'DEFLATE' }); @@ -118,7 +118,7 @@ describe('Document Parser', () => { path: tmpPath, mimetype: 'application/vnd.oasis.opendocument.text', } as Express.Multer.File; - await expect(parseDocument({ file })).rejects.toThrow(/exceeds the 50MB limit/); + await expect(parseDocument({ file })).rejects.toThrow(/exceeds the 50MB decompressed limit/); } finally { await fs.promises.unlink(tmpPath); } diff --git a/packages/api/src/files/documents/crud.ts b/packages/api/src/files/documents/crud.ts index e255323f77..20d547cf26 100644 --- a/packages/api/src/files/documents/crud.ts +++ b/packages/api/src/files/documents/crud.ts @@ -1,5 +1,5 @@ import * as fs from 'fs'; -import JSZip from 'jszip'; +import yauzl from 'yauzl'; import { megabyte, excelMimeTypes, FileSources } from 'librechat-data-provider'; import type { TextItem } from 'pdfjs-dist/types/src/display/api'; import type { MistralOCRUploadResult } from '~/types'; @@ -124,28 +124,7 @@ async function excelSheetToText(file: Express.Multer.File): Promise { * text boxes, and annotations are stripped without replacement. */ async function odtToText(file: Express.Multer.File): Promise { - const data = await fs.promises.readFile(file.path); - const zip = await JSZip.loadAsync(data); - - let totalUncompressed = 0; - zip.forEach((_, entry) => { - const raw = entry as JSZip.JSZipObject & { _data?: { uncompressedSize?: number } }; - // _data.uncompressedSize is populated from the ZIP central directory at parse time - // by jszip (private internal, jszip@3.x). If the field is absent the guard fails - // open (adds 0); this is an accepted limitation of the approach. - totalUncompressed += raw._data?.uncompressedSize ?? 0; - }); - if (totalUncompressed > ODT_MAX_DECOMPRESSED_SIZE) { - throw new Error( - `ODT file decompressed content (${Math.ceil(totalUncompressed / megabyte)}MB) exceeds the ${ODT_MAX_DECOMPRESSED_SIZE / megabyte}MB limit`, - ); - } - - const contentFile = zip.file('content.xml'); - if (!contentFile) { - throw new Error('ODT file is missing content.xml'); - } - const xml = await contentFile.async('string'); + const xml = await extractOdtContentXml(file.path); const bodyMatch = xml.match(/]*>([\s\S]*?)<\/office:body>/); if (!bodyMatch) { return ''; @@ -168,3 +147,85 @@ async function odtToText(file: Express.Multer.File): Promise { .replace(/\n{3,}/g, '\n\n') .trim(); } + +/** + * Streams content.xml out of an ODT ZIP archive using yauzl, counting real + * decompressed bytes and aborting mid-inflate if the cap is exceeded. + * Unlike JSZip metadata checks, this cannot be bypassed by falsifying + * the ZIP central directory's uncompressedSize fields. + * + * The zipfile is closed on all exit paths (success, size cap, missing entry, + * error) to prevent file descriptor leaks. + */ +function extractOdtContentXml(filePath: string): Promise { + return new Promise((resolve, reject) => { + yauzl.open(filePath, { lazyEntries: true }, (err, zipfile) => { + if (err) { + return reject(err); + } + if (!zipfile) { + return reject(new Error('Failed to open ODT file')); + } + + let settled = false; + const finish = (error: Error | null, result?: string) => { + if (settled) { + return; + } + settled = true; + zipfile.close(); + if (error) { + reject(error); + } else { + resolve(result as string); + } + }; + + let found = false; + zipfile.readEntry(); + + zipfile.on('entry', (entry: yauzl.Entry) => { + if (entry.fileName !== 'content.xml') { + zipfile.readEntry(); + return; + } + found = true; + zipfile.openReadStream(entry, (streamErr, readStream) => { + if (streamErr) { + return finish(streamErr); + } + if (!readStream) { + return finish(new Error('Failed to open content.xml stream')); + } + + let totalBytes = 0; + const chunks: Buffer[] = []; + + readStream.on('data', (chunk: Buffer) => { + totalBytes += chunk.byteLength; + if (totalBytes > ODT_MAX_DECOMPRESSED_SIZE) { + readStream.destroy( + new Error( + `ODT content.xml exceeds the ${ODT_MAX_DECOMPRESSED_SIZE / megabyte}MB decompressed limit`, + ), + ); + return; + } + chunks.push(chunk); + }); + + readStream.on('end', () => finish(null, Buffer.concat(chunks).toString('utf8'))); + readStream.on('error', (readErr: Error) => finish(readErr)); + }); + }); + + zipfile.on('end', () => { + if (!found) { + finish(new Error('ODT file is missing content.xml')); + } + }); + + zipfile.on('error', (zipErr: Error) => finish(zipErr)); + }); + }); +} From 594d9470d58c13843d499f126b37c1a88638f9f3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 20 Mar 2026 12:32:55 -0400 Subject: [PATCH 079/111] =?UTF-8?q?=F0=9F=AA=A4=20fix:=20Avoid=20express-r?= =?UTF-8?q?ate-limit=20v8=20ERR=5FERL=5FKEY=5FGEN=5FIPV6=20False=20Positiv?= =?UTF-8?q?e=20(#12333)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: avoid express-rate-limit v8 ERR_ERL_KEY_GEN_IPV6 false positive express-rate-limit v8 calls keyGenerator.toString() and throws ERR_ERL_KEY_GEN_IPV6 if the source contains the literal substring "req.ip" without "ipKeyGenerator". When packages/api compiles req?.ip to older JS targets, the output contains "req.ip", triggering the heuristic. Bracket notation (req?.['ip']) produces identical runtime behavior but never emits the literal "req.ip" substring regardless of compilation target. Closes #12321 * fix: add toString regression test and clean up redundant annotation Add a test that verifies removePorts.toString() does not contain "req.ip", guarding against reintroduction of the ERR_ERL_KEY_GEN_IPV6 false positive. Fix a misleading test description and remove a redundant type annotation on a trivially-inferred local. --- packages/api/src/utils/ports.spec.ts | 8 +++++++- packages/api/src/utils/ports.ts | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/api/src/utils/ports.spec.ts b/packages/api/src/utils/ports.spec.ts index ea4dc284c7..0a53c867ea 100644 --- a/packages/api/src/utils/ports.spec.ts +++ b/packages/api/src/utils/ports.spec.ts @@ -75,7 +75,7 @@ describe('removePorts', () => { expect(removePorts(req(undefined))).toBeUndefined(); }); - test('returns undefined when ip is empty string', () => { + test('returns empty string when ip is empty string', () => { expect(removePorts({ ip: '' } as Request)).toBe(''); }); @@ -90,6 +90,12 @@ describe('removePorts', () => { }); }); + describe('express-rate-limit v8 heuristic guard', () => { + test('function source does not contain "req.ip" (guards against ERR_ERL_KEY_GEN_IPV6)', () => { + expect(removePorts.toString()).not.toContain('req.ip'); + }); + }); + describe('unrecognized formats fall through unchanged', () => { test('returns garbage input unchanged', () => { expect(removePorts(req('not-an-ip'))).toBe('not-an-ip'); diff --git a/packages/api/src/utils/ports.ts b/packages/api/src/utils/ports.ts index ecd38039d6..ed20c89193 100644 --- a/packages/api/src/utils/ports.ts +++ b/packages/api/src/utils/ports.ts @@ -1,8 +1,12 @@ import type { Request } from 'express'; -/** Strips port suffix from req.ip for use as a rate-limiter key (IPv4 and IPv6-safe) */ +/** + * Strips port suffix from req.ip for use as a rate-limiter key (IPv4 and IPv6-safe). + * Bracket notation for the ip property avoids express-rate-limit v8's toString() + * heuristic that scans for the literal substring "req.ip" (ERR_ERL_KEY_GEN_IPV6). + */ export function removePorts(req: Request): string | undefined { - const ip = req?.ip; + const ip = req?.['ip']; if (!ip) { return ip; } From 96f6976e0039cce3be092d05198ef35830921034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Airam=20Hern=C3=A1ndez=20Hern=C3=A1ndez?= <100208966+Airamhh@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:46:57 +0000 Subject: [PATCH 080/111] =?UTF-8?q?=F0=9F=AA=82=20fix:=20Automatic=20`logo?= =?UTF-8?q?ut=5Fhint`=20Fallback=20for=20Oversized=20OpenID=20Token=20URLs?= =?UTF-8?q?=20(#12326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: automatic logout_hint fallback for long OpenID tokens Implements OIDC RP-Initiated Logout cascading strategy to prevent errors when id_token_hint makes logout URL too long. Automatically detects URLs exceeding configurable length and falls back to logout_hint only when URL is too long, preserving previous behavior when token is missing. Adds OPENID_MAX_LOGOUT_URL_LENGTH environment variable. Comprehensive test coverage with 20 tests. Works with any OpenID provider. * fix: address review findings for OIDC logout URL length fallback - Replace two-boolean tri-state (useIdTokenHint/urlTooLong) with a single string discriminant ('use_token'|'too_long'|'no_token') for clarity - Fix misleading warning: differentiate 'url too long + no client_id' from 'no token + no client_id' so operators get actionable advice - Strict env var parsing: reject partial numeric strings like '500abc' that Number.parseInt silently accepted; use regex + Number() instead - Pre-compute projected URL length from base URL + token length (JWT chars are URL-safe), eliminating the set-then-delete mutation pattern - Extract parseMaxLogoutUrlLength helper for validation and early return - Add tests: invalid env values, url-too-long + missing OPENID_CLIENT_ID, boundary condition (exact max vs max+1), cookie-sourced long token - Remove redundant try/finally in 'respects custom limit' test - Use empty value in .env.example to signal optional config (default: 2000) --------- Co-authored-by: Airam Hernández Hernández Co-authored-by: Danny Avila --- .env.example | 2 + .../controllers/auth/LogoutController.js | 78 ++++- .../controllers/auth/LogoutController.spec.js | 310 +++++++++++++++++- 3 files changed, 379 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index e746737ea4..73e95c394c 100644 --- a/.env.example +++ b/.env.example @@ -540,6 +540,8 @@ OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE="user.read" # example for Scope Needed for OPENID_USE_END_SESSION_ENDPOINT= # URL to redirect to after OpenID logout (defaults to ${DOMAIN_CLIENT}/login) OPENID_POST_LOGOUT_REDIRECT_URI= +# Maximum logout URL length before using logout_hint instead of id_token_hint (default: 2000) +OPENID_MAX_LOGOUT_URL_LENGTH= #========================# # SharePoint Integration # diff --git a/api/server/controllers/auth/LogoutController.js b/api/server/controllers/auth/LogoutController.js index 039ed630c2..381bfc58b2 100644 --- a/api/server/controllers/auth/LogoutController.js +++ b/api/server/controllers/auth/LogoutController.js @@ -4,11 +4,27 @@ const { logger } = require('@librechat/data-schemas'); const { logoutUser } = require('~/server/services/AuthService'); const { getOpenIdConfig } = require('~/strategies'); +/** Parses and validates OPENID_MAX_LOGOUT_URL_LENGTH, returning defaultValue on invalid input */ +function parseMaxLogoutUrlLength(defaultValue = 2000) { + const raw = process.env.OPENID_MAX_LOGOUT_URL_LENGTH; + const trimmed = raw == null ? '' : raw.trim(); + if (trimmed === '') { + return defaultValue; + } + const parsed = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN; + if (!Number.isFinite(parsed) || parsed <= 0) { + logger.warn( + `[logoutController] Invalid OPENID_MAX_LOGOUT_URL_LENGTH value "${raw}", using default ${defaultValue}`, + ); + return defaultValue; + } + return parsed; +} + const logoutController = async (req, res) => { const parsedCookies = req.headers.cookie ? cookies.parse(req.headers.cookie) : {}; const isOpenIdUser = req.user?.openidId != null && req.user?.provider === 'openid'; - /** For OpenID users, read tokens from session (with cookie fallback) */ let refreshToken; let idToken; if (isOpenIdUser && req.session?.openidTokens) { @@ -44,22 +60,64 @@ const logoutController = async (req, res) => { const endSessionEndpoint = openIdConfig.serverMetadata().end_session_endpoint; if (endSessionEndpoint) { const endSessionUrl = new URL(endSessionEndpoint); - /** Redirect back to app's login page after IdP logout */ const postLogoutRedirectUri = process.env.OPENID_POST_LOGOUT_REDIRECT_URI || `${process.env.DOMAIN_CLIENT}/login`; endSessionUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri); - /** Add id_token_hint (preferred) or client_id for OIDC spec compliance */ + /** + * OIDC RP-Initiated Logout cascading strategy: + * 1. id_token_hint (most secure, identifies exact session) + * 2. logout_hint + client_id (when URL would exceed safe length) + * 3. client_id only (when no token available) + * + * JWT tokens from spec-compliant OIDC providers use base64url + * encoding (RFC 7515), whose characters are all URL-safe, so + * token length equals URL-encoded length for projection. + * Non-compliant issuers using standard base64 (+/=) will cause + * underestimation; increase OPENID_MAX_LOGOUT_URL_LENGTH if the + * fallback does not trigger as expected. + */ + const maxLogoutUrlLength = parseMaxLogoutUrlLength(); + let strategy = 'no_token'; if (idToken) { + const baseLength = endSessionUrl.toString().length; + const projectedLength = baseLength + '&id_token_hint='.length + idToken.length; + if (projectedLength > maxLogoutUrlLength) { + strategy = 'too_long'; + logger.debug( + `[logoutController] Logout URL too long (${projectedLength} chars, max ${maxLogoutUrlLength}), ` + + 'switching to logout_hint strategy', + ); + } else { + strategy = 'use_token'; + } + } + + if (strategy === 'use_token') { endSessionUrl.searchParams.set('id_token_hint', idToken); - } else if (process.env.OPENID_CLIENT_ID) { - endSessionUrl.searchParams.set('client_id', process.env.OPENID_CLIENT_ID); } else { - logger.warn( - '[logoutController] Neither id_token_hint nor OPENID_CLIENT_ID is available. ' + - 'To enable id_token_hint, set OPENID_REUSE_TOKENS=true. ' + - 'The OIDC end-session request may be rejected by the identity provider.', - ); + if (strategy === 'too_long') { + const logoutHint = req.user?.email || req.user?.username || req.user?.openidId; + if (logoutHint) { + endSessionUrl.searchParams.set('logout_hint', logoutHint); + } + } + + if (process.env.OPENID_CLIENT_ID) { + endSessionUrl.searchParams.set('client_id', process.env.OPENID_CLIENT_ID); + } else if (strategy === 'too_long') { + logger.warn( + '[logoutController] Logout URL exceeds max length and OPENID_CLIENT_ID is not set. ' + + 'The OIDC end-session request may be rejected. ' + + 'Consider setting OPENID_CLIENT_ID or increasing OPENID_MAX_LOGOUT_URL_LENGTH.', + ); + } else { + logger.warn( + '[logoutController] Neither id_token_hint nor OPENID_CLIENT_ID is available. ' + + 'To enable id_token_hint, set OPENID_REUSE_TOKENS=true. ' + + 'The OIDC end-session request may be rejected by the identity provider.', + ); + } } response.redirect = endSessionUrl.toString(); diff --git a/api/server/controllers/auth/LogoutController.spec.js b/api/server/controllers/auth/LogoutController.spec.js index 3f2a2de8e1..c9294fdcec 100644 --- a/api/server/controllers/auth/LogoutController.spec.js +++ b/api/server/controllers/auth/LogoutController.spec.js @@ -1,7 +1,7 @@ const cookies = require('cookie'); const mockLogoutUser = jest.fn(); -const mockLogger = { warn: jest.fn(), error: jest.fn() }; +const mockLogger = { warn: jest.fn(), error: jest.fn(), debug: jest.fn() }; const mockIsEnabled = jest.fn(); const mockGetOpenIdConfig = jest.fn(); @@ -256,4 +256,312 @@ describe('LogoutController', () => { expect(res.clearCookie).toHaveBeenCalledWith('token_provider'); }); }); + + describe('URL length limit and logout_hint fallback', () => { + it('uses logout_hint when id_token makes URL exceed default limit (2000 chars)', async () => { + const longIdToken = 'a'.repeat(3000); + const req = buildReq({ + user: { _id: 'user1', openidId: 'oid1', provider: 'openid', email: 'user@example.com' }, + session: { + openidTokens: { refreshToken: 'srt', idToken: longIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('id_token_hint='); + expect(body.redirect).toContain('logout_hint=user%40example.com'); + expect(body.redirect).toContain('client_id=my-client-id'); + expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('Logout URL too long')); + }); + + it('uses id_token_hint when URL is within default limit', async () => { + const shortIdToken = 'short-token'; + const req = buildReq({ + session: { + openidTokens: { refreshToken: 'srt', idToken: shortIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint=short-token'); + expect(body.redirect).not.toContain('logout_hint='); + expect(body.redirect).not.toContain('client_id='); + }); + + it('respects custom OPENID_MAX_LOGOUT_URL_LENGTH', async () => { + process.env.OPENID_MAX_LOGOUT_URL_LENGTH = '500'; + const mediumIdToken = 'a'.repeat(600); + const req = buildReq({ + user: { _id: 'user1', openidId: 'oid1', provider: 'openid', email: 'user@example.com' }, + session: { + openidTokens: { refreshToken: 'srt', idToken: mediumIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('id_token_hint='); + expect(body.redirect).toContain('logout_hint=user%40example.com'); + }); + + it('uses username as logout_hint when email is not available', async () => { + const longIdToken = 'a'.repeat(3000); + const req = buildReq({ + user: { + _id: 'user1', + openidId: 'oid1', + provider: 'openid', + username: 'testuser', + }, + session: { + openidTokens: { refreshToken: 'srt', idToken: longIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('logout_hint=testuser'); + }); + + it('uses openidId as logout_hint when email and username are not available', async () => { + const longIdToken = 'a'.repeat(3000); + const req = buildReq({ + user: { _id: 'user1', openidId: 'unique-oid-123', provider: 'openid' }, + session: { + openidTokens: { refreshToken: 'srt', idToken: longIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('logout_hint=unique-oid-123'); + }); + + it('uses openidId as logout_hint when email and username are explicitly null', async () => { + const longIdToken = 'a'.repeat(3000); + const req = buildReq({ + user: { + _id: 'user1', + openidId: 'oid-without-email', + provider: 'openid', + email: null, + username: null, + }, + session: { + openidTokens: { refreshToken: 'srt', idToken: longIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('id_token_hint='); + expect(body.redirect).toContain('logout_hint=oid-without-email'); + expect(body.redirect).toContain('client_id=my-client-id'); + }); + + it('uses only client_id when absolutely no hint is available', async () => { + const longIdToken = 'a'.repeat(3000); + const req = buildReq({ + user: { + _id: 'user1', + openidId: '', + provider: 'openid', + email: '', + username: '', + }, + session: { + openidTokens: { refreshToken: 'srt', idToken: longIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('id_token_hint='); + expect(body.redirect).not.toContain('logout_hint='); + expect(body.redirect).toContain('client_id=my-client-id'); + }); + + it('warns about missing OPENID_CLIENT_ID when URL is too long', async () => { + delete process.env.OPENID_CLIENT_ID; + const longIdToken = 'a'.repeat(3000); + const req = buildReq({ + user: { _id: 'user1', openidId: 'oid1', provider: 'openid', email: 'user@example.com' }, + session: { + openidTokens: { refreshToken: 'srt', idToken: longIdToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('id_token_hint='); + expect(body.redirect).toContain('logout_hint='); + expect(body.redirect).not.toContain('client_id='); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('OPENID_CLIENT_ID is not set'), + ); + }); + + it('falls back to logout_hint for cookie-sourced long token', async () => { + const longCookieToken = 'a'.repeat(3000); + cookies.parse.mockReturnValue({ + refreshToken: 'cookie-rt', + openid_id_token: longCookieToken, + }); + const req = buildReq({ + user: { _id: 'user1', openidId: 'oid1', provider: 'openid', email: 'user@example.com' }, + session: { destroy: jest.fn() }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('id_token_hint='); + expect(body.redirect).toContain('logout_hint=user%40example.com'); + expect(body.redirect).toContain('client_id=my-client-id'); + }); + + it('keeps id_token_hint when projected URL length equals the max', async () => { + const baseUrl = new URL('https://idp.example.com/logout'); + baseUrl.searchParams.set('post_logout_redirect_uri', 'https://app.example.com/login'); + const baseLength = baseUrl.toString().length; + const tokenLength = 2000 - baseLength - '&id_token_hint='.length; + const exactToken = 'a'.repeat(tokenLength); + + const req = buildReq({ + session: { + openidTokens: { refreshToken: 'srt', idToken: exactToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint='); + expect(body.redirect).not.toContain('logout_hint='); + }); + + it('falls back to logout_hint when projected URL is one char over the max', async () => { + const baseUrl = new URL('https://idp.example.com/logout'); + baseUrl.searchParams.set('post_logout_redirect_uri', 'https://app.example.com/login'); + const baseLength = baseUrl.toString().length; + const tokenLength = 2000 - baseLength - '&id_token_hint='.length + 1; + const overToken = 'a'.repeat(tokenLength); + + const req = buildReq({ + user: { _id: 'user1', openidId: 'oid1', provider: 'openid', email: 'user@example.com' }, + session: { + openidTokens: { refreshToken: 'srt', idToken: overToken }, + destroy: jest.fn(), + }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('id_token_hint='); + expect(body.redirect).toContain('logout_hint='); + }); + }); + + describe('invalid OPENID_MAX_LOGOUT_URL_LENGTH values', () => { + it('silently uses default when value is empty', async () => { + process.env.OPENID_MAX_LOGOUT_URL_LENGTH = ''; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + expect(mockLogger.warn).not.toHaveBeenCalledWith( + expect.stringContaining('Invalid OPENID_MAX_LOGOUT_URL_LENGTH'), + ); + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint=small-id-token'); + }); + + it('warns and uses default for partial numeric string', async () => { + process.env.OPENID_MAX_LOGOUT_URL_LENGTH = '500abc'; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid OPENID_MAX_LOGOUT_URL_LENGTH'), + ); + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint=small-id-token'); + }); + + it('warns and uses default for zero value', async () => { + process.env.OPENID_MAX_LOGOUT_URL_LENGTH = '0'; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid OPENID_MAX_LOGOUT_URL_LENGTH'), + ); + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint=small-id-token'); + }); + + it('warns and uses default for negative value', async () => { + process.env.OPENID_MAX_LOGOUT_URL_LENGTH = '-1'; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid OPENID_MAX_LOGOUT_URL_LENGTH'), + ); + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint=small-id-token'); + }); + + it('warns and uses default for non-numeric string', async () => { + process.env.OPENID_MAX_LOGOUT_URL_LENGTH = 'abc'; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid OPENID_MAX_LOGOUT_URL_LENGTH'), + ); + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint=small-id-token'); + }); + }); }); From 54fc9c2c9958262e24967950d0e175d984388dd8 Mon Sep 17 00:00:00 2001 From: JooyoungChoi14 <63181822+JooyoungChoi14@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:47:51 +0900 Subject: [PATCH 081/111] =?UTF-8?q?=E2=99=BE=EF=B8=8F=20fix:=20Permanent?= =?UTF-8?q?=20Ban=20Cache=20and=20Expired=20Ban=20Cleanup=20Defects=20(#12?= =?UTF-8?q?324)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: preserve ban data object in checkBan to prevent permanent cache The !! operator on line 108 coerces the ban data object to a boolean, losing the expiresAt property. This causes: 1. Number(true.expiresAt) = NaN → expired bans never cleaned from banLogs 2. banCache.set(key, true, NaN) → Keyv stores with expires: null (permanent) 3. IP-based cache entries persist indefinitely, blocking unrelated users Fix: replace isBanned (boolean) with banData (original object) so that expiresAt is accessible for TTL calculation and proper cache expiry. * fix: address checkBan cleanup defects exposed by ban data fix The prior commit correctly replaced boolean coercion with the ban data object, but activated previously-dead cleanup code with several defects: - IP-only expired bans fell through cleanup without returning next(), re-caching with negative TTL (permanent entry) and blocking the user - Redis deployments used cache-prefixed keys for banLogs.delete(), silently failing since bans are stored at raw keys - banCache.set() calls were fire-and-forget, silently dropping errors - No guard for missing/invalid expiresAt reproduced the NaN TTL bug on legacy ban records Consolidate expired-ban cleanup into a single block that always returns next(), use raw keys (req.ip, userId) for banLogs.delete(), add an expiresAt validity guard, await cache writes with error logging, and parallelize independent I/O with Promise.all. Add 25 tests covering all checkBan code paths including the specific regressions for IP-only cleanup, Redis key mismatch, missing expiresAt, and cache write failures. --------- Co-authored-by: Danny Avila --- api/server/middleware/checkBan.js | 81 ++-- api/test/server/middleware/checkBan.test.js | 426 ++++++++++++++++++++ 2 files changed, 469 insertions(+), 38 deletions(-) create mode 100644 api/test/server/middleware/checkBan.test.js diff --git a/api/server/middleware/checkBan.js b/api/server/middleware/checkBan.js index 0c98f3a824..5d1b60297f 100644 --- a/api/server/middleware/checkBan.js +++ b/api/server/middleware/checkBan.js @@ -10,6 +10,14 @@ const { findUser } = require('~/models'); const banCache = new Keyv({ store: keyvMongo, namespace: ViolationTypes.BAN, ttl: 0 }); const message = 'Your account has been temporarily banned due to violations of our service.'; +/** @returns {string} Cache key for ban lookups, prefixed for Redis or raw for MongoDB */ +const getBanCacheKey = (prefix, value, useRedis) => { + if (!value) { + return ''; + } + return useRedis ? `ban_cache:${prefix}:${value}` : value; +}; + /** * Respond to the request if the user is banned. * @@ -63,25 +71,16 @@ const checkBan = async (req, res, next = () => {}) => { return next(); } - let cachedIPBan; - let cachedUserBan; + const useRedis = isEnabled(process.env.USE_REDIS); + const ipKey = getBanCacheKey('ip', req.ip, useRedis); + const userKey = getBanCacheKey('user', userId, useRedis); - let ipKey = ''; - let userKey = ''; + const [cachedIPBan, cachedUserBan] = await Promise.all([ + ipKey ? banCache.get(ipKey) : undefined, + userKey ? banCache.get(userKey) : undefined, + ]); - if (req.ip) { - ipKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:ip:${req.ip}` : req.ip; - cachedIPBan = await banCache.get(ipKey); - } - - if (userId) { - userKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:user:${userId}` : userId; - cachedUserBan = await banCache.get(userKey); - } - - const cachedBan = cachedIPBan || cachedUserBan; - - if (cachedBan) { + if (cachedIPBan || cachedUserBan) { req.banned = true; return await banResponse(req, res); } @@ -93,41 +92,47 @@ const checkBan = async (req, res, next = () => {}) => { return next(); } - let ipBan; - let userBan; + const [ipBan, userBan] = await Promise.all([ + req.ip ? banLogs.get(req.ip) : undefined, + userId ? banLogs.get(userId) : undefined, + ]); - if (req.ip) { - ipBan = await banLogs.get(req.ip); - } + const banData = ipBan || userBan; - if (userId) { - userBan = await banLogs.get(userId); - } - - const isBanned = !!(ipBan || userBan); - - if (!isBanned) { + if (!banData) { return next(); } - const timeLeft = Number(isBanned.expiresAt) - Date.now(); - - if (timeLeft <= 0 && ipKey) { - await banLogs.delete(ipKey); + const expiresAt = Number(banData.expiresAt); + if (!banData.expiresAt || isNaN(expiresAt)) { + req.banned = true; + return await banResponse(req, res); } - if (timeLeft <= 0 && userKey) { - await banLogs.delete(userKey); + const timeLeft = expiresAt - Date.now(); + + if (timeLeft <= 0) { + const cleanups = []; + if (ipBan) { + cleanups.push(banLogs.delete(req.ip)); + } + if (userBan) { + cleanups.push(banLogs.delete(userId)); + } + await Promise.all(cleanups); return next(); } + const cacheWrites = []; if (ipKey) { - banCache.set(ipKey, isBanned, timeLeft); + cacheWrites.push(banCache.set(ipKey, banData, timeLeft)); } - if (userKey) { - banCache.set(userKey, isBanned, timeLeft); + cacheWrites.push(banCache.set(userKey, banData, timeLeft)); } + await Promise.all(cacheWrites).catch((err) => + logger.warn('[checkBan] Failed to write ban cache:', err), + ); req.banned = true; return await banResponse(req, res); diff --git a/api/test/server/middleware/checkBan.test.js b/api/test/server/middleware/checkBan.test.js new file mode 100644 index 0000000000..518153be67 --- /dev/null +++ b/api/test/server/middleware/checkBan.test.js @@ -0,0 +1,426 @@ +const mockBanCacheGet = jest.fn().mockResolvedValue(undefined); +const mockBanCacheSet = jest.fn().mockResolvedValue(undefined); + +jest.mock('keyv', () => ({ + Keyv: jest.fn().mockImplementation(() => ({ + get: mockBanCacheGet, + set: mockBanCacheSet, + })), +})); + +const mockBanLogsGet = jest.fn().mockResolvedValue(undefined); +const mockBanLogsDelete = jest.fn().mockResolvedValue(true); +const mockBanLogs = { + get: mockBanLogsGet, + delete: mockBanLogsDelete, + opts: { ttl: 7200000 }, +}; + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(() => mockBanLogs), +})); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('@librechat/api', () => ({ + isEnabled: (value) => { + if (typeof value === 'boolean') { + return value; + } + if (typeof value === 'string') { + return value.toLowerCase().trim() === 'true'; + } + return false; + }, + keyvMongo: {}, + removePorts: jest.fn((req) => req.ip), +})); + +jest.mock('~/models', () => ({ + findUser: jest.fn(), +})); + +jest.mock('~/server/middleware/denyRequest', () => jest.fn().mockResolvedValue(undefined)); + +jest.mock('ua-parser-js', () => jest.fn(() => ({ browser: { name: 'Chrome' } }))); + +const checkBan = require('~/server/middleware/checkBan'); +const { logger } = require('@librechat/data-schemas'); +const { findUser } = require('~/models'); + +const createReq = (overrides = {}) => ({ + ip: '192.168.1.1', + user: { id: 'user123' }, + headers: { 'user-agent': 'Mozilla/5.0' }, + body: {}, + baseUrl: '/api', + originalUrl: '/api/test', + ...overrides, +}); + +const createRes = () => ({ + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), +}); + +describe('checkBan middleware', () => { + let originalEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + process.env.BAN_VIOLATIONS = 'true'; + delete process.env.USE_REDIS; + mockBanLogs.opts.ttl = 7200000; + }); + + afterEach(() => { + process.env = originalEnv; + jest.clearAllMocks(); + }); + + describe('early exits', () => { + it('calls next() when BAN_VIOLATIONS is disabled', async () => { + process.env.BAN_VIOLATIONS = 'false'; + const next = jest.fn(); + + await checkBan(createReq(), createRes(), next); + + expect(next).toHaveBeenCalledWith(); + expect(mockBanCacheGet).not.toHaveBeenCalled(); + }); + + it('calls next() when BAN_VIOLATIONS is unset', async () => { + delete process.env.BAN_VIOLATIONS; + const next = jest.fn(); + + await checkBan(createReq(), createRes(), next); + + expect(next).toHaveBeenCalledWith(); + }); + + it('calls next() when neither userId nor IP is available', async () => { + const next = jest.fn(); + const req = createReq({ ip: null, user: null }); + + await checkBan(req, createRes(), next); + + expect(next).toHaveBeenCalledWith(); + }); + + it('calls next() when ban duration is <= 0', async () => { + mockBanLogs.opts.ttl = 0; + const next = jest.fn(); + + await checkBan(createReq(), createRes(), next); + + expect(next).toHaveBeenCalledWith(); + }); + + it('calls next() when no ban exists in cache or DB', async () => { + const next = jest.fn(); + + await checkBan(createReq(), createRes(), next); + + expect(next).toHaveBeenCalledWith(); + expect(mockBanCacheGet).toHaveBeenCalled(); + expect(mockBanLogsGet).toHaveBeenCalled(); + }); + }); + + describe('cache hit path', () => { + it('returns 403 when IP ban is cached', async () => { + mockBanCacheGet.mockResolvedValueOnce({ expiresAt: Date.now() + 60000 }); + const next = jest.fn(); + const req = createReq(); + const res = createRes(); + + await checkBan(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(req.banned).toBe(true); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('returns 403 when user ban is cached (IP miss)', async () => { + mockBanCacheGet + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce({ expiresAt: Date.now() + 60000 }); + const next = jest.fn(); + const req = createReq(); + const res = createRes(); + + await checkBan(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(req.banned).toBe(true); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('does not query banLogs when cache hit occurs', async () => { + mockBanCacheGet.mockResolvedValueOnce({ expiresAt: Date.now() + 60000 }); + + await checkBan(createReq(), createRes(), jest.fn()); + + expect(mockBanLogsGet).not.toHaveBeenCalled(); + }); + }); + + describe('active ban (positive timeLeft)', () => { + it('caches ban with correct TTL and returns 403', async () => { + const expiresAt = Date.now() + 3600000; + const banRecord = { expiresAt, type: 'ban', violation_count: 3 }; + mockBanLogsGet.mockResolvedValueOnce(banRecord); + const next = jest.fn(); + const req = createReq(); + const res = createRes(); + + await checkBan(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(req.banned).toBe(true); + expect(res.status).toHaveBeenCalledWith(403); + expect(mockBanCacheSet).toHaveBeenCalledTimes(2); + + const [ipCacheCall, userCacheCall] = mockBanCacheSet.mock.calls; + expect(ipCacheCall[0]).toBe('192.168.1.1'); + expect(ipCacheCall[1]).toBe(banRecord); + expect(ipCacheCall[2]).toBeGreaterThan(0); + expect(ipCacheCall[2]).toBeLessThanOrEqual(3600000); + + expect(userCacheCall[0]).toBe('user123'); + expect(userCacheCall[1]).toBe(banRecord); + }); + + it('caches only IP when no userId is present', async () => { + const expiresAt = Date.now() + 3600000; + mockBanLogsGet.mockResolvedValueOnce({ expiresAt, type: 'ban' }); + const req = createReq({ user: null }); + + await checkBan(req, createRes(), jest.fn()); + + expect(mockBanCacheSet).toHaveBeenCalledTimes(1); + expect(mockBanCacheSet).toHaveBeenCalledWith( + '192.168.1.1', + expect.any(Object), + expect.any(Number), + ); + }); + }); + + describe('expired ban cleanup', () => { + it('cleans up and calls next() for expired user-key ban', async () => { + const expiresAt = Date.now() - 1000; + mockBanLogsGet + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce({ expiresAt, type: 'ban' }); + const next = jest.fn(); + const req = createReq(); + + await checkBan(req, createRes(), next); + + expect(next).toHaveBeenCalledWith(); + expect(req.banned).toBeUndefined(); + expect(mockBanLogsDelete).toHaveBeenCalledWith('user123'); + expect(mockBanCacheSet).not.toHaveBeenCalled(); + }); + + it('cleans up and calls next() for expired IP-only ban (Finding 1 regression)', async () => { + const expiresAt = Date.now() - 1000; + mockBanLogsGet.mockResolvedValueOnce({ expiresAt, type: 'ban' }); + const next = jest.fn(); + const req = createReq({ user: null }); + + await checkBan(req, createRes(), next); + + expect(next).toHaveBeenCalledWith(); + expect(req.banned).toBeUndefined(); + expect(mockBanLogsDelete).toHaveBeenCalledWith('192.168.1.1'); + expect(mockBanCacheSet).not.toHaveBeenCalled(); + }); + + it('cleans up both IP and user bans when both are expired', async () => { + const expiresAt = Date.now() - 1000; + mockBanLogsGet + .mockResolvedValueOnce({ expiresAt, type: 'ban' }) + .mockResolvedValueOnce({ expiresAt, type: 'ban' }); + const next = jest.fn(); + + await checkBan(createReq(), createRes(), next); + + expect(next).toHaveBeenCalledWith(); + expect(mockBanLogsDelete).toHaveBeenCalledTimes(2); + expect(mockBanLogsDelete).toHaveBeenCalledWith('192.168.1.1'); + expect(mockBanLogsDelete).toHaveBeenCalledWith('user123'); + }); + + it('does not write to banCache when ban is expired', async () => { + const expiresAt = Date.now() - 60000; + mockBanLogsGet.mockResolvedValueOnce({ expiresAt, type: 'ban' }); + + await checkBan(createReq({ user: null }), createRes(), jest.fn()); + + expect(mockBanCacheSet).not.toHaveBeenCalled(); + }); + }); + + describe('Redis key paths (Finding 2 regression)', () => { + beforeEach(() => { + process.env.USE_REDIS = 'true'; + }); + + it('uses cache-prefixed keys for banCache.get', async () => { + await checkBan(createReq(), createRes(), jest.fn()); + + expect(mockBanCacheGet).toHaveBeenCalledWith('ban_cache:ip:192.168.1.1'); + expect(mockBanCacheGet).toHaveBeenCalledWith('ban_cache:user:user123'); + }); + + it('uses raw keys (not cache-prefixed) for banLogs.delete on cleanup', async () => { + const expiresAt = Date.now() - 1000; + mockBanLogsGet + .mockResolvedValueOnce({ expiresAt, type: 'ban' }) + .mockResolvedValueOnce({ expiresAt, type: 'ban' }); + + await checkBan(createReq(), createRes(), jest.fn()); + + expect(mockBanLogsDelete).toHaveBeenCalledWith('192.168.1.1'); + expect(mockBanLogsDelete).toHaveBeenCalledWith('user123'); + for (const call of mockBanLogsDelete.mock.calls) { + expect(call[0]).not.toMatch(/^ban_cache:/); + } + }); + + it('uses cache-prefixed keys for banCache.set on active ban', async () => { + const expiresAt = Date.now() + 3600000; + mockBanLogsGet.mockResolvedValueOnce({ expiresAt, type: 'ban' }); + + await checkBan(createReq(), createRes(), jest.fn()); + + expect(mockBanCacheSet).toHaveBeenCalledWith( + 'ban_cache:ip:192.168.1.1', + expect.any(Object), + expect.any(Number), + ); + expect(mockBanCacheSet).toHaveBeenCalledWith( + 'ban_cache:user:user123', + expect.any(Object), + expect.any(Number), + ); + }); + }); + + describe('missing expiresAt guard (Finding 5)', () => { + it('returns 403 without caching when expiresAt is missing', async () => { + mockBanLogsGet.mockResolvedValueOnce({ type: 'ban' }); + const next = jest.fn(); + const req = createReq(); + const res = createRes(); + + await checkBan(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(req.banned).toBe(true); + expect(res.status).toHaveBeenCalledWith(403); + expect(mockBanCacheSet).not.toHaveBeenCalled(); + }); + + it('returns 403 without caching when expiresAt is NaN-producing', async () => { + mockBanLogsGet.mockResolvedValueOnce({ type: 'ban', expiresAt: 'not-a-number' }); + const next = jest.fn(); + const res = createRes(); + + await checkBan(createReq(), res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(mockBanCacheSet).not.toHaveBeenCalled(); + }); + + it('returns 403 without caching when expiresAt is null', async () => { + mockBanLogsGet.mockResolvedValueOnce({ type: 'ban', expiresAt: null }); + const next = jest.fn(); + const res = createRes(); + + await checkBan(createReq(), res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(mockBanCacheSet).not.toHaveBeenCalled(); + }); + }); + + describe('cache write error handling (Finding 4)', () => { + it('still returns 403 when banCache.set rejects', async () => { + const expiresAt = Date.now() + 3600000; + mockBanLogsGet.mockResolvedValueOnce({ expiresAt, type: 'ban' }); + mockBanCacheSet.mockRejectedValue(new Error('MongoDB write failure')); + const next = jest.fn(); + const req = createReq(); + const res = createRes(); + + await checkBan(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(req.banned).toBe(true); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('logs a warning when banCache.set fails', async () => { + const expiresAt = Date.now() + 3600000; + mockBanLogsGet.mockResolvedValueOnce({ expiresAt, type: 'ban' }); + mockBanCacheSet.mockRejectedValue(new Error('write failed')); + + await checkBan(createReq(), createRes(), jest.fn()); + + expect(logger.warn).toHaveBeenCalledWith( + '[checkBan] Failed to write ban cache:', + expect.any(Error), + ); + }); + }); + + describe('user lookup by email', () => { + it('resolves userId from email when not on request', async () => { + const req = createReq({ user: null, body: { email: 'test@example.com' } }); + findUser.mockResolvedValueOnce({ _id: 'resolved-user-id' }); + const expiresAt = Date.now() + 3600000; + mockBanLogsGet + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce({ expiresAt, type: 'ban' }); + + await checkBan(req, createRes(), jest.fn()); + + expect(findUser).toHaveBeenCalledWith({ email: 'test@example.com' }, '_id'); + expect(req.banned).toBe(true); + }); + + it('continues with IP-only check when email lookup finds no user', async () => { + const req = createReq({ user: null, body: { email: 'unknown@example.com' } }); + findUser.mockResolvedValueOnce(null); + const next = jest.fn(); + + await checkBan(req, createRes(), next); + + expect(next).toHaveBeenCalledWith(); + }); + }); + + describe('error handling', () => { + it('calls next(error) when an unexpected error occurs', async () => { + mockBanCacheGet.mockRejectedValueOnce(new Error('connection lost')); + const next = jest.fn(); + + await checkBan(createReq(), createRes(), next); + + expect(next).toHaveBeenCalledWith(expect.any(Error)); + expect(logger.error).toHaveBeenCalled(); + }); + }); +}); From 28c2e224ae0fa0dccf355ff010a89ffb51838a7b Mon Sep 17 00:00:00 2001 From: ethanlaj Date: Fri, 20 Mar 2026 13:06:04 -0400 Subject: [PATCH 082/111] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Resolve=20corre?= =?UTF-8?q?ct=20memory=20directory=20in=20`.gitignore`=20(#12330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Exclude memory directory from gitignore for API package * fix: Scope memory/ and coordination/ gitignore to repo root Prefix patterns with `/` so they only match root-level Claude Flow artifact directories, not workspace source like packages/api/src/memory/. --------- Co-authored-by: Danny Avila --- .gitignore | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 86d4a3ddae..980be5b8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -154,16 +154,16 @@ claude-flow.config.json .swarm/ .hive-mind/ .claude-flow/ -memory/ -coordination/ -memory/claude-flow-data.json -memory/sessions/* -!memory/sessions/README.md -memory/agents/* -!memory/agents/README.md -coordination/memory_bank/* -coordination/subtasks/* -coordination/orchestration/* +/memory/ +/coordination/ +/memory/claude-flow-data.json +/memory/sessions/* +!/memory/sessions/README.md +/memory/agents/* +!/memory/agents/README.md +/coordination/memory_bank/* +/coordination/subtasks/* +/coordination/orchestration/* *.db *.db-journal *.db-wal From 4e5ae28fa90063a36d755fa217d6a0f517a6cc12 Mon Sep 17 00:00:00 2001 From: mfish911 <33205066+mfish911@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:07:39 -0400 Subject: [PATCH 083/111] =?UTF-8?q?=F0=9F=93=A1=20feat:=20Support=20Unauth?= =?UTF-8?q?enticated=20SMTP=20Relays=20(#12322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allow smtp server that does not have authentication * fix: align checkEmailConfig with optional SMTP credentials and add tests Remove EMAIL_USERNAME/EMAIL_PASSWORD requirements from the hasSMTPConfig predicate in checkEmailConfig() so the rest of the codebase (login, startup checks, invite-user) correctly recognizes unauthenticated SMTP as a valid email configuration. Add a warning when only one of the two credential env vars is set, in both sendEmail.js and checkEmailConfig(), to catch partial misconfigurations early. Add test coverage for both the transporter auth assembly in sendEmail.js and the checkEmailConfig predicate in packages/api. Document in .env.example that credentials are optional for unauthenticated SMTP relays. --------- Co-authored-by: Danny Avila --- .env.example | 1 + api/server/utils/__tests__/sendEmail.spec.js | 143 ++++++++++++++++++ api/server/utils/sendEmail.js | 15 +- .../api/src/utils/__tests__/email.test.ts | 120 +++++++++++++++ packages/api/src/utils/email.ts | 17 ++- 5 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 api/server/utils/__tests__/sendEmail.spec.js create mode 100644 packages/api/src/utils/__tests__/email.test.ts diff --git a/.env.example b/.env.example index 73e95c394c..ae3537038a 100644 --- a/.env.example +++ b/.env.example @@ -625,6 +625,7 @@ EMAIL_PORT=25 EMAIL_ENCRYPTION= EMAIL_ENCRYPTION_HOSTNAME= EMAIL_ALLOW_SELFSIGNED= +# Leave both empty for SMTP servers that do not require authentication EMAIL_USERNAME= EMAIL_PASSWORD= EMAIL_FROM_NAME= diff --git a/api/server/utils/__tests__/sendEmail.spec.js b/api/server/utils/__tests__/sendEmail.spec.js new file mode 100644 index 0000000000..5c79094c53 --- /dev/null +++ b/api/server/utils/__tests__/sendEmail.spec.js @@ -0,0 +1,143 @@ +const nodemailer = require('nodemailer'); +const { readFileAsString } = require('@librechat/api'); + +jest.mock('nodemailer'); +jest.mock('@librechat/data-schemas', () => ({ + logger: { debug: jest.fn(), warn: jest.fn(), error: jest.fn() }, +})); +jest.mock('@librechat/api', () => ({ + logAxiosError: jest.fn(), + isEnabled: jest.fn((val) => val === 'true' || val === true), + readFileAsString: jest.fn(), +})); + +const savedEnv = { ...process.env }; + +const mockSendMail = jest.fn().mockResolvedValue({ messageId: 'test-id' }); + +beforeEach(() => { + jest.clearAllMocks(); + process.env = { ...savedEnv }; + process.env.EMAIL_HOST = 'smtp.example.com'; + process.env.EMAIL_PORT = '587'; + process.env.EMAIL_FROM = 'noreply@example.com'; + process.env.APP_TITLE = 'TestApp'; + delete process.env.EMAIL_USERNAME; + delete process.env.EMAIL_PASSWORD; + delete process.env.MAILGUN_API_KEY; + delete process.env.MAILGUN_DOMAIN; + delete process.env.EMAIL_SERVICE; + delete process.env.EMAIL_ENCRYPTION; + delete process.env.EMAIL_ENCRYPTION_HOSTNAME; + delete process.env.EMAIL_ALLOW_SELFSIGNED; + + readFileAsString.mockResolvedValue({ content: '

{{name}}

' }); + nodemailer.createTransport.mockReturnValue({ sendMail: mockSendMail }); +}); + +afterAll(() => { + process.env = savedEnv; +}); + +/** Loads a fresh copy of sendEmail so process.env reads are re-evaluated. */ +function loadSendEmail() { + jest.resetModules(); + jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockReturnValue({ sendMail: mockSendMail }), + })); + jest.mock('@librechat/data-schemas', () => ({ + logger: { debug: jest.fn(), warn: jest.fn(), error: jest.fn() }, + })); + jest.mock('@librechat/api', () => ({ + logAxiosError: jest.fn(), + isEnabled: jest.fn((val) => val === 'true' || val === true), + readFileAsString: jest.fn().mockResolvedValue({ content: '

{{name}}

' }), + })); + return require('../sendEmail'); +} + +const baseParams = { + email: 'user@example.com', + subject: 'Test', + payload: { name: 'User' }, + template: 'test.handlebars', +}; + +describe('sendEmail SMTP auth assembly', () => { + it('includes auth when both EMAIL_USERNAME and EMAIL_PASSWORD are set', async () => { + process.env.EMAIL_USERNAME = 'smtp_user'; + process.env.EMAIL_PASSWORD = 'smtp_pass'; + const sendEmail = loadSendEmail(); + const { createTransport } = require('nodemailer'); + + await sendEmail(baseParams); + + expect(createTransport).toHaveBeenCalledTimes(1); + const transporterOptions = createTransport.mock.calls[0][0]; + expect(transporterOptions.auth).toEqual({ + user: 'smtp_user', + pass: 'smtp_pass', + }); + }); + + it('omits auth when both EMAIL_USERNAME and EMAIL_PASSWORD are absent', async () => { + const sendEmail = loadSendEmail(); + const { createTransport } = require('nodemailer'); + + await sendEmail(baseParams); + + expect(createTransport).toHaveBeenCalledTimes(1); + const transporterOptions = createTransport.mock.calls[0][0]; + expect(transporterOptions.auth).toBeUndefined(); + }); + + it('omits auth and logs a warning when only EMAIL_USERNAME is set', async () => { + process.env.EMAIL_USERNAME = 'smtp_user'; + const sendEmail = loadSendEmail(); + const { createTransport } = require('nodemailer'); + const { logger: freshLogger } = require('@librechat/data-schemas'); + + await sendEmail(baseParams); + + const transporterOptions = createTransport.mock.calls[0][0]; + expect(transporterOptions.auth).toBeUndefined(); + expect(freshLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('EMAIL_USERNAME and EMAIL_PASSWORD must both be set'), + ); + }); + + it('omits auth and logs a warning when only EMAIL_PASSWORD is set', async () => { + process.env.EMAIL_PASSWORD = 'smtp_pass'; + const sendEmail = loadSendEmail(); + const { createTransport } = require('nodemailer'); + const { logger: freshLogger } = require('@librechat/data-schemas'); + + await sendEmail(baseParams); + + const transporterOptions = createTransport.mock.calls[0][0]; + expect(transporterOptions.auth).toBeUndefined(); + expect(freshLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('EMAIL_USERNAME and EMAIL_PASSWORD must both be set'), + ); + }); + + it('does not log a warning when both credentials are properly set', async () => { + process.env.EMAIL_USERNAME = 'smtp_user'; + process.env.EMAIL_PASSWORD = 'smtp_pass'; + const sendEmail = loadSendEmail(); + const { logger: freshLogger } = require('@librechat/data-schemas'); + + await sendEmail(baseParams); + + expect(freshLogger.warn).not.toHaveBeenCalled(); + }); + + it('does not log a warning when both credentials are absent', async () => { + const sendEmail = loadSendEmail(); + const { logger: freshLogger } = require('@librechat/data-schemas'); + + await sendEmail(baseParams); + + expect(freshLogger.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/api/server/utils/sendEmail.js b/api/server/utils/sendEmail.js index 432a571ffb..3fa3e6fcba 100644 --- a/api/server/utils/sendEmail.js +++ b/api/server/utils/sendEmail.js @@ -124,11 +124,20 @@ const sendEmail = async ({ email, subject, payload, template, throwError = true // Whether to accept unsigned certificates rejectUnauthorized: !isEnabled(process.env.EMAIL_ALLOW_SELFSIGNED), }, - auth: { + }; + + const hasUsername = !!process.env.EMAIL_USERNAME; + const hasPassword = !!process.env.EMAIL_PASSWORD; + if (hasUsername && hasPassword) { + transporterOptions.auth = { user: process.env.EMAIL_USERNAME, pass: process.env.EMAIL_PASSWORD, - }, - }; + }; + } else if (hasUsername !== hasPassword) { + logger.warn( + '[sendEmail] EMAIL_USERNAME and EMAIL_PASSWORD must both be set for authenticated SMTP, or both omitted for unauthenticated SMTP. Proceeding without authentication.', + ); + } if (process.env.EMAIL_ENCRYPTION_HOSTNAME) { // Check the certificate against this name explicitly diff --git a/packages/api/src/utils/__tests__/email.test.ts b/packages/api/src/utils/__tests__/email.test.ts new file mode 100644 index 0000000000..ccbd0aabfe --- /dev/null +++ b/packages/api/src/utils/__tests__/email.test.ts @@ -0,0 +1,120 @@ +import { logger } from '@librechat/data-schemas'; +import { checkEmailConfig } from '../email'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { warn: jest.fn() }, +})); + +const savedEnv = { ...process.env }; + +beforeEach(() => { + jest.clearAllMocks(); + process.env = { ...savedEnv }; + delete process.env.EMAIL_SERVICE; + delete process.env.EMAIL_HOST; + delete process.env.EMAIL_USERNAME; + delete process.env.EMAIL_PASSWORD; + delete process.env.EMAIL_FROM; + delete process.env.MAILGUN_API_KEY; + delete process.env.MAILGUN_DOMAIN; +}); + +afterAll(() => { + process.env = savedEnv; +}); + +describe('checkEmailConfig', () => { + describe('SMTP configuration', () => { + it('returns true with EMAIL_HOST and EMAIL_FROM (no credentials)', () => { + process.env.EMAIL_HOST = 'smtp.example.com'; + process.env.EMAIL_FROM = 'noreply@example.com'; + expect(checkEmailConfig()).toBe(true); + }); + + it('returns true with EMAIL_SERVICE and EMAIL_FROM (no credentials)', () => { + process.env.EMAIL_SERVICE = 'gmail'; + process.env.EMAIL_FROM = 'noreply@example.com'; + expect(checkEmailConfig()).toBe(true); + }); + + it('returns true with EMAIL_HOST, EMAIL_FROM, and full credentials', () => { + process.env.EMAIL_HOST = 'smtp.example.com'; + process.env.EMAIL_FROM = 'noreply@example.com'; + process.env.EMAIL_USERNAME = 'user'; + process.env.EMAIL_PASSWORD = 'pass'; + expect(checkEmailConfig()).toBe(true); + }); + + it('returns false when EMAIL_FROM is missing', () => { + process.env.EMAIL_HOST = 'smtp.example.com'; + expect(checkEmailConfig()).toBe(false); + }); + + it('returns false when neither EMAIL_HOST nor EMAIL_SERVICE is set', () => { + process.env.EMAIL_FROM = 'noreply@example.com'; + expect(checkEmailConfig()).toBe(false); + }); + + it('returns false when no email env vars are set', () => { + expect(checkEmailConfig()).toBe(false); + }); + }); + + describe('partial credential warning', () => { + it('logs a warning when only EMAIL_USERNAME is set', () => { + process.env.EMAIL_HOST = 'smtp.example.com'; + process.env.EMAIL_FROM = 'noreply@example.com'; + process.env.EMAIL_USERNAME = 'user'; + checkEmailConfig(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('EMAIL_USERNAME and EMAIL_PASSWORD must both be set'), + ); + }); + + it('logs a warning when only EMAIL_PASSWORD is set', () => { + process.env.EMAIL_HOST = 'smtp.example.com'; + process.env.EMAIL_FROM = 'noreply@example.com'; + process.env.EMAIL_PASSWORD = 'pass'; + checkEmailConfig(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('EMAIL_USERNAME and EMAIL_PASSWORD must both be set'), + ); + }); + + it('does not warn when both credentials are set', () => { + process.env.EMAIL_HOST = 'smtp.example.com'; + process.env.EMAIL_FROM = 'noreply@example.com'; + process.env.EMAIL_USERNAME = 'user'; + process.env.EMAIL_PASSWORD = 'pass'; + checkEmailConfig(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('does not warn when neither credential is set', () => { + process.env.EMAIL_HOST = 'smtp.example.com'; + process.env.EMAIL_FROM = 'noreply@example.com'; + checkEmailConfig(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('does not warn for partial credentials when SMTP is not configured', () => { + process.env.EMAIL_USERNAME = 'user'; + checkEmailConfig(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + }); + + describe('Mailgun configuration', () => { + it('returns true with Mailgun API key, domain, and EMAIL_FROM', () => { + process.env.MAILGUN_API_KEY = 'key-abc123'; + process.env.MAILGUN_DOMAIN = 'mg.example.com'; + process.env.EMAIL_FROM = 'noreply@example.com'; + expect(checkEmailConfig()).toBe(true); + }); + + it('returns false when Mailgun is partially configured', () => { + process.env.MAILGUN_API_KEY = 'key-abc123'; + expect(checkEmailConfig()).toBe(false); + }); + }); +}); diff --git a/packages/api/src/utils/email.ts b/packages/api/src/utils/email.ts index f98e7c51be..6f9171a43b 100644 --- a/packages/api/src/utils/email.ts +++ b/packages/api/src/utils/email.ts @@ -1,3 +1,5 @@ +import { logger } from '@librechat/data-schemas'; + /** * Check if email configuration is set * @returns Returns `true` if either Mailgun or SMTP is properly configured @@ -7,10 +9,17 @@ export function checkEmailConfig(): boolean { !!process.env.MAILGUN_API_KEY && !!process.env.MAILGUN_DOMAIN && !!process.env.EMAIL_FROM; const hasSMTPConfig = - (!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) && - !!process.env.EMAIL_USERNAME && - !!process.env.EMAIL_PASSWORD && - !!process.env.EMAIL_FROM; + (!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) && !!process.env.EMAIL_FROM; + + if (hasSMTPConfig) { + const hasUsername = !!process.env.EMAIL_USERNAME; + const hasPassword = !!process.env.EMAIL_PASSWORD; + if (hasUsername !== hasPassword) { + logger.warn( + '[checkEmailConfig] EMAIL_USERNAME and EMAIL_PASSWORD must both be set for authenticated SMTP, or both omitted for unauthenticated SMTP.', + ); + } + } return hasMailgunConfig || hasSMTPConfig; } From 59873e74fc3cd6fe43230e59342599c9c1f8fadb Mon Sep 17 00:00:00 2001 From: YE <69640321+JasonYeYuhe@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:08:48 +0900 Subject: [PATCH 084/111] =?UTF-8?q?=F0=9F=8F=AE=20docs:=20Add=20Simplified?= =?UTF-8?q?=20Chinese=20README=20Translation=20(#12323)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add Simplified Chinese translation (README.zh.md) * docs: sync README.zh.md with current English README Address review findings: - Fix stale Railway URLs (railway.app -> railway.com, /template/ -> /deploy/) - Add missing Resumable Streams section - Add missing sub-features (Agent Marketplace, Collaborative Sharing, Jina Reranking, prompt sharing, Helicone provider) - Update multilingual UI language list from 18 to 30+ languages - Replace outdated "ChatGPT clone" platform description with current messaging - Remove stale video embed no longer in English README - Remove non-standard bold wrappers on links - Fix trailing whitespace - Add sync header with date and commit SHA --------- Co-authored-by: Danny Avila --- README.md | 5 ++ README.zh.md | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 README.zh.md diff --git a/README.md b/README.md index e82b3ebc2c..7da34974e3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@

+

+ English · + 中文 +

+

+ +

+ + + +

+ LibreChat +

+

+ +

+ English · + 中文 +

+ +

+ + + + + + + + + + + + +

+ +

+ + Deploy on Railway + + + Deploy on Zeabur + + + Deploy on Sealos + +

+ +

+ + 翻译进度 + +

+ + +# ✨ 功能 + +- 🖥️ **UI 与体验**:受 ChatGPT 启发,并具备更强的设计与功能。 + +- 🤖 **AI 模型选择**: + - Anthropic (Claude), AWS Bedrock, OpenAI, Azure OpenAI, Google, Vertex AI, OpenAI Responses API (包含 Azure) + - [自定义端点 (Custom Endpoints)](https://www.librechat.ai/docs/quick_start/custom_endpoints):LibreChat 支持任何兼容 OpenAI 规范的 API,无需代理。 + - 兼容[本地与远程 AI 服务商](https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints): + - Ollama, groq, Cohere, Mistral AI, Apple MLX, koboldcpp, together.ai, + - OpenRouter, Helicone, Perplexity, ShuttleAI, Deepseek, Qwen 等。 + +- 🔧 **[代码解释器 (Code Interpreter) API](https://www.librechat.ai/docs/features/code_interpreter)**: + - 安全的沙箱执行环境,支持 Python, Node.js (JS/TS), Go, C/C++, Java, PHP, Rust 和 Fortran。 + - 无缝文件处理:直接上传、处理并下载文件。 + - 隐私无忧:完全隔离且安全的执行环境。 + +- 🔦 **智能体与工具集成**: + - **[LibreChat 智能体 (Agents)](https://www.librechat.ai/docs/features/agents)**: + - 无代码定制助手:无需编程即可构建专业化的 AI 驱动助手。 + - 智能体市场:发现并部署社区构建的智能体。 + - 协作共享:与特定用户和群组共享智能体。 + - 灵活且可扩展:支持 MCP 服务器、工具、文件搜索、代码执行等。 + - 兼容自定义端点、OpenAI, Azure, Anthropic, AWS Bedrock, Google, Vertex AI, Responses API 等。 + - [支持模型上下文协议 (MCP)](https://modelcontextprotocol.io/clients#librechat) 用于工具调用。 + +- 🔍 **网页搜索**: + - 搜索互联网并检索相关信息以增强 AI 上下文。 + - 结合搜索提供商、内容爬虫和结果重排序,确保最佳检索效果。 + - **可定制 Jina 重排序**:配置自定义 Jina API URL 用于重排序服务。 + - **[了解更多 →](https://www.librechat.ai/docs/features/web_search)** + +- 🪄 **支持代码 Artifacts 的生成式 UI**: + - [代码 Artifacts](https://youtu.be/GfTj7O4gmd0?si=WJbdnemZpJzBrJo3) 允许在对话中直接创建 React 组件、HTML 页面和 Mermaid 图表。 + +- 🎨 **图像生成与编辑**: + - 使用 [GPT-Image-1](https://www.librechat.ai/docs/features/image_gen#1--openai-image-tools-recommended) 进行文生图与图生图。 + - 支持 [DALL-E (3/2)](https://www.librechat.ai/docs/features/image_gen#2--dalle-legacy), [Stable Diffusion](https://www.librechat.ai/docs/features/image_gen#3--stable-diffusion-local), [Flux](https://www.librechat.ai/docs/features/image_gen#4--flux) 或任何 [MCP 服务器](https://www.librechat.ai/docs/features/image_gen#5--model-context-protocol-mcp)。 + - 根据提示词生成惊艳的视觉效果,或通过指令精修现有图像。 + +- 💾 **预设与上下文管理**: + - 创建、保存并分享自定义预设。 + - 在对话中随时切换 AI 端点和预设。 + - 编辑、重新提交并通过对话分支继续消息。 + - 创建并与特定用户和群组共享提示词。 + - [消息与对话分叉 (Fork)](https://www.librechat.ai/docs/features/fork) 以实现高级上下文控制。 + +- 💬 **多模态与文件交互**: + - 使用 Claude 3, GPT-4.5, GPT-4o, o1, Llama-Vision 和 Gemini 上传并分析图像 📸。 + - 支持通过自定义端点、OpenAI, Azure, Anthropic, AWS Bedrock 和 Google 进行文件对话 🗃️。 + +- 🌎 **多语言 UI**: + - English, 中文 (简体), 中文 (繁體), العربية, Deutsch, Español, Français, Italiano + - Polski, Português (PT), Português (BR), Русский, 日本語, Svenska, 한국어, Tiếng Việt + - Türkçe, Nederlands, עברית, Català, Čeština, Dansk, Eesti, فارسی + - Suomi, Magyar, Հայերեն, Bahasa Indonesia, ქართული, Latviešu, ไทย, ئۇيغۇرچە + +- 🧠 **推理 UI**: + - 针对 DeepSeek-R1 等思维链/推理 AI 模型的动态推理 UI。 + +- 🎨 **可定制界面**: + - 可定制的下拉菜单和界面,同时适配高级用户和初学者。 + +- 🌊 **[可恢复流 (Resumable Streams)](https://www.librechat.ai/docs/features/resumable_streams)**: + - 永不丢失响应:AI 响应在连接中断后自动重连并继续。 + - 多标签页与多设备同步:在多个标签页打开同一对话,或在另一设备上继续。 + - 生产级可靠性:支持从单机部署到基于 Redis 的水平扩展。 + +- 🗣️ **语音与音频**: + - 通过语音转文字和文字转语音实现免提对话。 + - 自动发送并播放音频。 + - 支持 OpenAI, Azure OpenAI 和 Elevenlabs。 + +- 📥 **导入与导出对话**: + - 从 LibreChat, ChatGPT, Chatbot UI 导入对话。 + - 将对话导出为截图、Markdown、文本、JSON。 + +- 🔍 **搜索与发现**: + - 搜索所有消息和对话。 + +- 👥 **多用户与安全访问**: + - 支持 OAuth2, LDAP 和电子邮件登录的多用户安全认证。 + - 内置审核系统和 Token 消耗管理工具。 + +- ⚙️ **配置与部署**: + - 支持代理、反向代理、Docker 及多种部署选项。 + - 可完全本地运行或部署在云端。 + +- 📖 **开源与社区**: + - 完全开源且在公众监督下开发。 + - 社区驱动的开发、支持与反馈。 + +[查看我们的文档了解更多功能详情](https://docs.librechat.ai/) 📚 + +## 🪶 LibreChat:全方位的 AI 对话平台 + +LibreChat 是一个自托管的 AI 对话平台,在一个注重隐私的统一界面中整合了所有主流 AI 服务商。 + +除了对话功能外,LibreChat 还提供 AI 智能体、模型上下文协议 (MCP) 支持、Artifacts、代码解释器、自定义操作、对话搜索,以及企业级多用户认证。 + +开源、活跃开发中,专为重视 AI 基础设施自主可控的用户而构建。 + +--- + +## 🌐 资源 + +**GitHub 仓库:** + - **RAG API:** [github.com/danny-avila/rag_api](https://github.com/danny-avila/rag_api) + - **网站:** [github.com/LibreChat-AI/librechat.ai](https://github.com/LibreChat-AI/librechat.ai) + +**其他:** + - **官方网站:** [librechat.ai](https://librechat.ai) + - **帮助文档:** [librechat.ai/docs](https://librechat.ai/docs) + - **博客:** [librechat.ai/blog](https://librechat.ai/blog) + +--- + +## 📝 更新日志 + +访问发布页面和更新日志以了解最新动态: +- [发布页面 (Releases)](https://github.com/danny-avila/LibreChat/releases) +- [更新日志 (Changelog)](https://www.librechat.ai/changelog) + +**⚠️ 在更新前请务必查看[更新日志](https://www.librechat.ai/changelog)以了解破坏性更改。** + +--- + +## ⭐ Star 历史 + +

+ + Star History Chart + +

+

+ + danny-avila%2FLibreChat | Trendshift + + + ROSS Index - 2024年第一季度增长最快的开源初创公司 | Runa Capital + +

+ +--- + +## ✨ 贡献 + +欢迎任何形式的贡献、建议、错误报告和修复! + +对于新功能、组件或扩展,请在发送 PR 前开启 issue 进行讨论。 + +如果您想帮助我们将 LibreChat 翻译成您的母语,我们非常欢迎!改进翻译不仅能让全球用户更轻松地使用 LibreChat,还能提升整体用户体验。请查看我们的[翻译指南](https://www.librechat.ai/docs/translation)。 + +--- + +## 💖 感谢所有贡献者 + + + + + +--- + +## 🎉 特别鸣谢 + +感谢 [Locize](https://locize.com) 提供的翻译管理工具,支持 LibreChat 的多语言功能。 + +

+ + Locize Logo + +

From b66f7914a5f62ea478ba9ccd3ce98b83bfc1bf52 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 20 Mar 2026 13:31:08 -0400 Subject: [PATCH 085/111] =?UTF-8?q?=E2=9B=93=EF=B8=8F=E2=80=8D=F0=9F=92=A5?= =?UTF-8?q?=20fix:=20Replace=20React=20Markdown=20Artifact=20Renderer=20wi?= =?UTF-8?q?th=20Static=20HTML=20(#12337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The react-markdown dependency chain uses Node.js subpath imports (vfile/lib/#minpath) that Sandpack's bundler cannot resolve, breaking markdown artifact preview. Switch to a self-contained static HTML page using marked.js from CDN, eliminating the React bootstrap overhead and the problematic dependency resolution. --- .../__tests__/useArtifactProps.test.ts | 33 +-- client/src/utils/__tests__/markdown.test.ts | 201 +++++++++++------- client/src/utils/artifacts.ts | 18 +- client/src/utils/markdown.ts | 170 +++++++-------- 4 files changed, 224 insertions(+), 198 deletions(-) diff --git a/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts index e46a285c50..5ffd52879f 100644 --- a/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts +++ b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts @@ -19,7 +19,7 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); expect(result.current.fileKey).toBe('content.md'); - expect(result.current.template).toBe('react-ts'); + expect(result.current.template).toBe('static'); }); it('should handle text/plain type with content.md as fileKey', () => { @@ -31,7 +31,7 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); expect(result.current.fileKey).toBe('content.md'); - expect(result.current.template).toBe('react-ts'); + expect(result.current.template).toBe('static'); }); it('should include content.md in files with original markdown', () => { @@ -46,7 +46,7 @@ describe('useArtifactProps', () => { expect(result.current.files['content.md']).toBe(markdownContent); }); - it('should include App.tsx with wrapped markdown renderer', () => { + it('should include index.html with static markdown rendering', () => { const artifact = createArtifact({ type: 'text/markdown', content: '# Test', @@ -54,8 +54,8 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); - expect(result.current.files['App.tsx']).toContain('MarkdownRenderer'); - expect(result.current.files['App.tsx']).toContain('import React from'); + expect(result.current.files['index.html']).toContain(''); + expect(result.current.files['index.html']).toContain('marked.parse'); }); it('should include all required markdown files', () => { @@ -66,12 +66,8 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); - // Check all required files are present expect(result.current.files['content.md']).toBeDefined(); - expect(result.current.files['App.tsx']).toBeDefined(); - expect(result.current.files['index.tsx']).toBeDefined(); - expect(result.current.files['/components/ui/MarkdownRenderer.tsx']).toBeDefined(); - expect(result.current.files['markdown.css']).toBeDefined(); + expect(result.current.files['index.html']).toBeDefined(); }); it('should escape special characters in markdown content', () => { @@ -82,13 +78,11 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); - // Original content should be preserved in content.md expect(result.current.files['content.md']).toContain('`const x = 1;`'); expect(result.current.files['content.md']).toContain('C:\\Users'); - // App.tsx should have escaped content - expect(result.current.files['App.tsx']).toContain('\\`'); - expect(result.current.files['App.tsx']).toContain('\\\\'); + expect(result.current.files['index.html']).toContain('\\`'); + expect(result.current.files['index.html']).toContain('\\\\'); }); it('should handle empty markdown content', () => { @@ -112,7 +106,7 @@ describe('useArtifactProps', () => { expect(result.current.files['content.md']).toBe('# No content provided'); }); - it('should provide react-markdown dependency', () => { + it('should have no custom dependencies for markdown (uses CDN)', () => { const artifact = createArtifact({ type: 'text/markdown', content: '# Test', @@ -120,9 +114,8 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); - expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('react-markdown'); - expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('remark-gfm'); - expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('remark-breaks'); + const deps = result.current.sharedProps.customSetup?.dependencies ?? {}; + expect(deps).toEqual({}); }); it('should update files when content changes', () => { @@ -137,7 +130,6 @@ describe('useArtifactProps', () => { expect(result.current.files['content.md']).toBe('# Original'); - // Update the artifact content const updatedArtifact = createArtifact({ ...artifact, content: '# Updated', @@ -201,8 +193,6 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); - // Language parameter should not affect markdown handling - // It checks the type directly, not the key expect(result.current.fileKey).toBe('content.md'); expect(result.current.files['content.md']).toBe('# Test'); }); @@ -214,7 +204,6 @@ describe('useArtifactProps', () => { const { result } = renderHook(() => useArtifactProps({ artifact })); - // Should use default behavior expect(result.current.template).toBe('static'); }); }); diff --git a/client/src/utils/__tests__/markdown.test.ts b/client/src/utils/__tests__/markdown.test.ts index 9734e0e18a..9834f034e9 100644 --- a/client/src/utils/__tests__/markdown.test.ts +++ b/client/src/utils/__tests__/markdown.test.ts @@ -1,4 +1,4 @@ -import { isSafeUrl, getMarkdownFiles } from '../markdown'; +import { isSafeUrl, getMarkdownFiles, EMBEDDED_IS_SAFE_URL } from '../markdown'; describe('isSafeUrl', () => { it('allows https URLs', () => { @@ -68,6 +68,37 @@ describe('isSafeUrl', () => { }); }); +describe('isSafeUrl sync verification', () => { + const embeddedFn = new Function('url', EMBEDDED_IS_SAFE_URL + '\nreturn isSafeUrl(url);') as ( + url: string, + ) => boolean; + + const cases: [string, boolean][] = [ + ['https://example.com', true], + ['http://example.com', true], + ['mailto:a@b.com', true], + ['tel:+1234567890', true], + ['/relative', true], + ['./relative', true], + ['../up', true], + ['#anchor', true], + ['javascript:alert(1)', false], + [' javascript:void(0)', false], + ['data:text/html,x', false], + ['blob:http://x.com/uuid', false], + ['vbscript:run', false], + ['file:///etc/passwd', false], + ['custom:payload', false], + ['', false], + [' ', false], + ]; + + it.each(cases)('embedded copy matches exported isSafeUrl for %j → %s', (url, expected) => { + expect(embeddedFn(url)).toBe(expected); + expect(isSafeUrl(url)).toBe(expected); + }); +}); + describe('markdown artifacts', () => { describe('getMarkdownFiles', () => { it('should return content.md with the original markdown content', () => { @@ -83,46 +114,26 @@ describe('markdown artifacts', () => { expect(files['content.md']).toBe('# No content provided'); }); - it('should include App.tsx with MarkdownRenderer component', () => { + it('should include index.html with static markdown rendering', () => { const markdown = '# Test'; const files = getMarkdownFiles(markdown); - expect(files['App.tsx']).toContain('import React from'); - expect(files['App.tsx']).toContain( - "import MarkdownRenderer from '/components/ui/MarkdownRenderer'", - ); - expect(files['App.tsx']).toContain(''); + expect(files['index.html']).toContain('marked.min.js'); + expect(files['index.html']).toContain('marked.parse'); + expect(files['index.html']).toContain('# Test'); }); - it('should include index.tsx entry point', () => { - const markdown = '# Test'; - const files = getMarkdownFiles(markdown); - - expect(files['index.tsx']).toContain('import App from "./App"'); - expect(files['index.tsx']).toContain('import "./styles.css"'); - expect(files['index.tsx']).toContain('import "./markdown.css"'); - expect(files['index.tsx']).toContain('createRoot'); + it('should only produce content.md and index.html', () => { + const files = getMarkdownFiles('# Test'); + expect(Object.keys(files).sort()).toEqual(['content.md', 'index.html']); }); - it('should include MarkdownRenderer component file', () => { - const markdown = '# Test'; - const files = getMarkdownFiles(markdown); - - expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('import ReactMarkdown from'); - expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('MarkdownRendererProps'); - expect(files['/components/ui/MarkdownRenderer.tsx']).toContain( - 'export default MarkdownRenderer', - ); - }); - - it('should include markdown.css with styling', () => { - const markdown = '# Test'; - const files = getMarkdownFiles(markdown); - - expect(files['markdown.css']).toContain('.markdown-body'); - expect(files['markdown.css']).toContain('list-style-type: disc'); - expect(files['markdown.css']).toContain('prefers-color-scheme: dark'); + it('should include markdown CSS in index.html', () => { + const files = getMarkdownFiles('# Test'); + expect(files['index.html']).toContain('.markdown-body'); + expect(files['index.html']).toContain('list-style-type: disc'); + expect(files['index.html']).toContain('prefers-color-scheme: dark'); }); describe('content escaping', () => { @@ -130,29 +141,36 @@ describe('markdown artifacts', () => { const markdown = 'Here is some `inline code`'; const files = getMarkdownFiles(markdown); - expect(files['App.tsx']).toContain('\\`'); + expect(files['index.html']).toContain('\\`'); }); it('should escape backslashes in markdown content', () => { const markdown = 'Path: C:\\Users\\Test'; const files = getMarkdownFiles(markdown); - expect(files['App.tsx']).toContain('\\\\'); + expect(files['index.html']).toContain('\\\\'); }); it('should escape dollar signs in markdown content', () => { const markdown = 'Price: $100'; const files = getMarkdownFiles(markdown); - expect(files['App.tsx']).toContain('\\$'); + expect(files['index.html']).toContain('\\$'); }); it('should handle code blocks with backticks', () => { const markdown = '```js\nconsole.log("test");\n```'; const files = getMarkdownFiles(markdown); - // Should be escaped - expect(files['App.tsx']).toContain('\\`\\`\\`'); + expect(files['index.html']).toContain('\\`\\`\\`'); + }); + + it('should prevent in content from breaking out of the script block', () => { + const markdown = 'Some content with '; + const files = getMarkdownFiles(markdown); + + expect(files['index.html']).not.toContain(' + + +`; +} + +export const getMarkdownFiles = (content: string): Record => { + const md = content || '# No content provided'; return { - 'content.md': content || '# No content provided', - 'App.tsx': wrapMarkdownRenderer(content), - 'index.tsx': dedent(`import React, { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./styles.css"; -import "./markdown.css"; - -import App from "./App"; - -const root = createRoot(document.getElementById("root")); -root.render(); -;`), - '/components/ui/MarkdownRenderer.tsx': markdownRenderer, - 'markdown.css': markdownCSS, + 'content.md': md, + 'index.html': generateMarkdownHtml(md), }; }; From 729ba96100d301172e0e6caaeaddfd41bb64f3a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:43:25 -0400 Subject: [PATCH 086/111] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#12338)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/de/translation.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index 71f0c453a6..1f59df94e6 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -236,6 +236,7 @@ "com_endpoint_assistant": "Assistent", "com_endpoint_assistant_model": "Assistentenmodell", "com_endpoint_assistant_placeholder": "Bitte wähle einen Assistenten aus dem rechten Seitenpanel aus", + "com_endpoint_bedrock_reasoning_effort": "Steuert die Intensität des Nachdenkens für unterstützte Bedrock-Modelle (z. B. Kimi K2.5, GLM). Höhere Stufen führen zu gründlicherem Nachdenken auf Kosten von erhöhter Latenz und mehr Tokens.", "com_endpoint_config_click_here": "Klicke hier", "com_endpoint_config_google_api_info": "Um deinen Generative Language API-Key (für Gemini) zu erhalten,", "com_endpoint_config_google_api_key": "Google API-Key", @@ -274,8 +275,9 @@ "com_endpoint_google_custom_name_placeholder": "Lege einen benutzerdefinierten Namen für Google fest", "com_endpoint_google_maxoutputtokens": "Maximale Anzahl von Token, die in der Antwort generiert werden können. Gib einen niedrigeren Wert für kürzere Antworten und einen höheren Wert für längere Antworten an. Hinweis: Modelle können möglicherweise vor Erreichen dieses Maximums stoppen.", "com_endpoint_google_temp": "Höhere Werte = zufälliger, während niedrigere Werte = fokussierter und deterministischer. Wir empfehlen, entweder dies oder Top P zu ändern, aber nicht beides.", - "com_endpoint_google_thinking": "Aktiviert oder deaktiviert die Argumentation. Diese Einstellung wird nur von bestimmten Modellen (Serie 2.5) unterstützt. Bei älteren Modellen hat diese Einstellung möglicherweise keine Wirkung.", - "com_endpoint_google_thinking_budget": "Gibt die Anzahl der Tokens an, die das Modell \"zum Nachdenken\" verwendet. Die tatsächliche Anzahl kann je nach Eingabeaufforderung diesen Wert über- oder unterschreiten.\n\nDiese Einstellung wird nur von bestimmten Modellen (2.5-Serie) unterstützt. Gemini 2.5 Pro unterstützt 128–32.768 Token. Gemini 2.5 Flash unterstützt 0–24.576 Token. Gemini 2.5 Flash Lite unterstützt 512–24.576 Token.\n\nLeer lassen oder auf „-1“ setzen, damit das Modell automatisch entscheidet, wann und wie viel nachgedacht werden soll. Standardmäßig denkt Gemini 2.5 Flash Lite nicht.", + "com_endpoint_google_thinking": "Aktiviert oder deaktiviert das Nachdenken. Unterstützt von den Gemini 2.5- und 3-Serien. Hinweis: Bei Gemini 3 Pro kann das Nachdenken nicht vollständig deaktiviert werden.", + "com_endpoint_google_thinking_budget": "Steuert die Anzahl der Token, die das Modell zum Nachdenken verwendet. Die tatsächliche Menge kann je nach Eingabe über oder unter diesem Wert liegen.\n\nDiese Einstellung gilt nur für Gemini 2.5 und ältere Modelle. Verwende für Gemini 3 und neuer stattdessen die Einstellung „Nachdenk-Level“.\n\nGemini 2.5 Pro unterstützt 128–32.768 Token. Gemini 2.5 Flash unterstützt 0–24.576 Token. Gemini 2.5 Flash Lite unterstützt 512–24.576 Token.\n\nLass das Feld leer oder setze den Wert auf „-1“, damit das Modell automatisch entscheidet, wann und wie viel es nachdenkt. Standardmäßig denkt Gemini 2.5 Flash Lite nicht nach.", + "com_endpoint_google_thinking_level": "Steuert die Tiefe des Nachdenkens für Gemini 3 und neuere Modelle. Hat keine Auswirkung auf Gemini 2.5 und ältere Modelle — nutze für diese das Denk-Budget.\n\nBelasse die Einstellung auf „Auto“, um den Standard des Modells zu verwenden.", "com_endpoint_google_topk": "Top-k ändert, wie das Modell Token für die Antwort auswählt. Ein Top-k von 1 bedeutet, dass das ausgewählte Token das wahrscheinlichste unter allen Token im Vokabular des Modells ist (auch Greedy-Decoding genannt), während ein Top-k von 3 bedeutet, dass das nächste Token aus den 3 wahrscheinlichsten Token ausgewählt wird (unter Verwendung der Temperatur).", "com_endpoint_google_topp": "Top-p ändert, wie das Modell Token für die Antwort auswählt. Token werden von den wahrscheinlichsten K (siehe topK-Parameter) bis zu den am wenigsten wahrscheinlichen ausgewählt, bis die Summe ihrer Wahrscheinlichkeiten dem Top-p-Wert entspricht.", "com_endpoint_google_use_search_grounding": "Nutze die Google-Suche, um Antworten mit Echtzeit-Ergebnissen aus dem Web zu verbessern. Dies ermöglicht es den Modellen, auf aktuelle Informationen zuzugreifen und präzisere, aktuellere Antworten zu geben.", @@ -345,6 +347,7 @@ "com_endpoint_temperature": "Temperatur", "com_endpoint_thinking": "Denken", "com_endpoint_thinking_budget": "Denkbudget", + "com_endpoint_thinking_level": "Nachdenk-Level", "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Aktiven Assistenten verwenden", @@ -365,6 +368,7 @@ "com_error_illegal_model_request": "Das Modell „{{0}}“ ist für {{1}} nicht verfügbar. Bitte wähle ein anderes Modell aus.", "com_error_input_length": "Die Anzahl der Tokens der letzten Nachricht ist zu lang und überschreitet das Token-Limit. Oder Ihre Token-Limit-Parameter sind falsch konfiguriert, was sich negativ auf das Kontextfenster auswirkt. Weitere Informationen: {{0}}. Bitte kürzen Sie Ihre Nachricht, passen Sie die maximale Kontextgröße in den Konversationsparametern an oder teilen Sie die Konversation auf, um fortzufahren.", "com_error_invalid_agent_provider": "Der Anbieter \"{{0}}\" steht für die Verwendung mit Agents nicht zur Verfügung. Bitte gehe zu den Einstellungen deines Agents und wähle einen aktuell verfügbaren Anbieter aus.", + "com_error_invalid_base_url": "Die angegebene Basis-URL verweist auf eine eingeschränkte Adresse. Bitte verwende eine gültige externe URL und versuche es erneut.", "com_error_invalid_user_key": "Ungültiger API-Key angegeben. Bitte gebe einen gültigen API-Key ein und versuche es erneut.", "com_error_missing_model": "Kein Modell für {{0}} ausgewählt. Bitte wähle ein Modell und versuch es erneut.", "com_error_models_not_loaded": "Die Modellkonfiguration konnte nicht geladen werden. Bitte lade die Seite neu und versuch es erneut.", @@ -636,6 +640,7 @@ "com_ui_2fa_generate_error": "Beim Erstellen der Einstellungen für die Zwei-Faktor-Authentifizierung ist ein Fehler aufgetreten.", "com_ui_2fa_invalid": "Ungültiger Zwei-Faktor-Authentifizierungscode.", "com_ui_2fa_setup": "2FA einrichten", + "com_ui_2fa_verification_required": "Gib deinen 2FA-Code ein, um fortzufahren", "com_ui_2fa_verified": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich verifiziert.", "com_ui_accept": "Ich akzeptiere", "com_ui_action_button": "Aktions Button", @@ -743,8 +748,10 @@ "com_ui_at_least_one_owner_required": "Mindestens ein Besitzer ist erforderlich.", "com_ui_attach_error": "Datei kann nicht angehängt werden. Erstelle oder wähle einen Chat oder versuche, die Seite zu aktualisieren.", "com_ui_attach_error_disabled": "Datei-Uploads sind für diesen Endpunkt deaktiviert", + "com_ui_attach_error_limit": "Dateilimit erreicht:", "com_ui_attach_error_openai": "Assistentendateien können nicht an andere Endpunkte angehängt werden", "com_ui_attach_error_size": "Dateigrößenlimit für Endpunkt überschritten:", + "com_ui_attach_error_total_size": "Limit der Gesamtdateigröße für den Endpunkt überschritten:", "com_ui_attach_error_type": "Nicht unterstützter Dateityp für Endpunkt:", "com_ui_attach_remove": "Datei entfernen", "com_ui_attach_warn_endpoint": "Nicht-Assistentendateien werden möglicherweise ohne kompatibles Werkzeug ignoriert", @@ -842,6 +849,7 @@ "com_ui_controls": "Steuerung", "com_ui_conversation": "Konversation", "com_ui_conversation_label": "{{title}} Konversation", + "com_ui_conversation_not_found": "Chat nicht gefunden", "com_ui_conversations": "Konversationen", "com_ui_convo_archived": "Konversation archiviert", "com_ui_convo_delete_error": "Unterhaltung konnte nicht gelöscht werden.", @@ -1185,7 +1193,7 @@ "com_ui_next": "Weiter", "com_ui_no": "Nein", "com_ui_no_api_keys": "Noch keine API-Schlüssel vorhanden. Erstelle einen, um loszulegen.", - "com_ui_no_auth": "Keine Auth", + "com_ui_no_auth": "Keine (Automatische Erkennung)", "com_ui_no_bookmarks": "Du hast noch keine Lesezeichen. Klicke auf einen Chat und füge ein neues hinzu", "com_ui_no_bookmarks_match": "Keine Lesezeichen entsprechen deiner Suche", "com_ui_no_bookmarks_title": "Noch keine Lesezeichen", From 69764144649d3a7011f6f9a376088ecc4709a06c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 20 Mar 2026 16:50:12 -0400 Subject: [PATCH 087/111] =?UTF-8?q?=F0=9F=97=A3=EF=B8=8F=20a11y:=20Disting?= =?UTF-8?q?uish=20Conversation=20Headings=20for=20Screen=20Readers=20(#123?= =?UTF-8?q?41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: distinguish message headings for screen readers Before, each message would have the heading of either the name of the user or the name of the agent (e.g. "Dan Lew" or "Claude Sonnet"). If you tried to navigate that with a screen reader, you'd just see a ton of headings switching back and forth between the two with no way to figure out where in the conversation each is. Now, we prefix each header with whether it's a "prompt" or "response", plus we number them so that you can distinguish how far in the conversation each part is. (This is a screen reader only change - there's no visual difference.) * fix: patch MessageParts heading, guard negative depth, add tests - Add sr-only heading prefix to MessageParts.tsx (Assistants endpoint path) - Extract shared getMessageNumber helper to avoid DRY violation between getMessageAriaLabel and getHeaderPrefixForScreenReader - Guard against depth < 0 producing "Prompt 0:" / "Response 0:" - Remove unused lodash import - Add unit tests covering all branches including depth edge cases --------- Co-authored-by: Dan Lew --- .../components/Chat/Messages/MessageParts.tsx | 5 +- .../Chat/Messages/ui/MessageRender.tsx | 9 +- .../src/components/Messages/ContentRender.tsx | 7 +- client/src/utils/__tests__/messages.test.ts | 82 +++++++++++++++++++ client/src/utils/messages.ts | 29 ++++++- 5 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 client/src/utils/__tests__/messages.test.ts diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx index 7aa73a54e6..3d13fa6ae0 100644 --- a/client/src/components/Chat/Messages/MessageParts.tsx +++ b/client/src/components/Chat/Messages/MessageParts.tsx @@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil'; import type { TMessageContentParts } from 'librechat-data-provider'; import type { TMessageProps, TMessageIcon } from '~/common'; import { useMessageHelpers, useLocalize, useAttachments, useContentMetadata } from '~/hooks'; +import { cn, getHeaderPrefixForScreenReader, getMessageAriaLabel } from '~/utils'; import MessageIcon from '~/components/Chat/Messages/MessageIcon'; import ContentParts from './Content/ContentParts'; import { fontSizeAtom } from '~/store/fontSize'; @@ -11,7 +12,6 @@ import SiblingSwitch from './SiblingSwitch'; import MultiMessage from './MultiMessage'; import HoverButtons from './HoverButtons'; import SubRow from './SubRow'; -import { cn, getMessageAriaLabel } from '~/utils'; import store from '~/store'; export default function Message(props: TMessageProps) { @@ -125,6 +125,9 @@ export default function Message(props: TMessageProps) { > {!hasParallelContent && (

+ + {getHeaderPrefixForScreenReader(message, localize)} + {name}

)} diff --git a/client/src/components/Chat/Messages/ui/MessageRender.tsx b/client/src/components/Chat/Messages/ui/MessageRender.tsx index e261a576bd..93586f0d2f 100644 --- a/client/src/components/Chat/Messages/ui/MessageRender.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRender.tsx @@ -1,8 +1,9 @@ import React, { useCallback, useMemo, memo } from 'react'; import { useAtomValue } from 'jotai'; import { useRecoilValue } from 'recoil'; -import { type TMessage } from 'librechat-data-provider'; +import type { TMessage } from 'librechat-data-provider'; import type { TMessageProps, TMessageIcon } from '~/common'; +import { cn, getHeaderPrefixForScreenReader, getMessageAriaLabel } from '~/utils'; import MessageContent from '~/components/Chat/Messages/Content/MessageContent'; import { useLocalize, useMessageActions, useContentMetadata } from '~/hooks'; import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow'; @@ -10,7 +11,6 @@ import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch'; import HoverButtons from '~/components/Chat/Messages/HoverButtons'; import MessageIcon from '~/components/Chat/Messages/MessageIcon'; import SubRow from '~/components/Chat/Messages/SubRow'; -import { cn, getMessageAriaLabel } from '~/utils'; import { fontSizeAtom } from '~/store/fontSize'; import { MessageContext } from '~/Providers'; import store from '~/store'; @@ -148,7 +148,10 @@ const MessageRender = memo(function MessageRender({ )} > {!hasParallelContent && ( -

{messageLabel}

+

+ {getHeaderPrefixForScreenReader(msg, localize)} + {messageLabel} +

)}
diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx index 4114baefe4..6b3f05ce5d 100644 --- a/client/src/components/Messages/ContentRender.tsx +++ b/client/src/components/Messages/ContentRender.tsx @@ -4,13 +4,13 @@ import { useRecoilValue } from 'recoil'; import type { TMessage, TMessageContentParts } from 'librechat-data-provider'; import type { TMessageProps, TMessageIcon } from '~/common'; import { useAttachments, useLocalize, useMessageActions, useContentMetadata } from '~/hooks'; +import { cn, getHeaderPrefixForScreenReader, getMessageAriaLabel } from '~/utils'; import ContentParts from '~/components/Chat/Messages/Content/ContentParts'; import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow'; import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch'; import HoverButtons from '~/components/Chat/Messages/HoverButtons'; import MessageIcon from '~/components/Chat/Messages/MessageIcon'; import SubRow from '~/components/Chat/Messages/SubRow'; -import { cn, getMessageAriaLabel } from '~/utils'; import { fontSizeAtom } from '~/store/fontSize'; import store from '~/store'; @@ -140,7 +140,10 @@ const ContentRender = memo(function ContentRender({ )} > {!hasParallelContent && ( -

{messageLabel}

+

+ {getHeaderPrefixForScreenReader(msg, localize)} + {messageLabel} +

)}
diff --git a/client/src/utils/__tests__/messages.test.ts b/client/src/utils/__tests__/messages.test.ts new file mode 100644 index 0000000000..4af9f69439 --- /dev/null +++ b/client/src/utils/__tests__/messages.test.ts @@ -0,0 +1,82 @@ +import type { TMessage } from 'librechat-data-provider'; +import type { LocalizeFunction } from '~/common'; +import { getMessageAriaLabel, getHeaderPrefixForScreenReader } from '../messages'; + +const translations: Record = { + com_endpoint_message: 'Message', + com_endpoint_message_new: 'Message {{0}}', + com_ui_prompt: 'Prompt', + com_ui_response: 'Response', +}; + +const localize: LocalizeFunction = ((key: string, args?: Record) => { + const template = translations[key] ?? key; + if (args) { + return Object.entries(args).reduce( + (result, [k, v]) => result.replace(`{{${k}}}`, String(v)), + template, + ); + } + return template; +}) as LocalizeFunction; + +const makeMessage = (overrides: Partial = {}): TMessage => + ({ + messageId: 'msg-1', + isCreatedByUser: false, + ...overrides, + }) as TMessage; + +describe('getMessageAriaLabel', () => { + it('returns "Message N" when depth is present and valid', () => { + const msg = makeMessage({ depth: 2 }); + expect(getMessageAriaLabel(msg, localize)).toBe('Message 3'); + }); + + it('returns "Message" when depth is undefined', () => { + const msg = makeMessage({ depth: undefined }); + expect(getMessageAriaLabel(msg, localize)).toBe('Message'); + }); + + it('returns "Message" when depth is negative', () => { + const msg = makeMessage({ depth: -1 }); + expect(getMessageAriaLabel(msg, localize)).toBe('Message'); + }); + + it('returns "Message 1" for depth 0 (root message)', () => { + const msg = makeMessage({ depth: 0 }); + expect(getMessageAriaLabel(msg, localize)).toBe('Message 1'); + }); +}); + +describe('getHeaderPrefixForScreenReader', () => { + it('returns "Prompt N: " for user messages with valid depth', () => { + const msg = makeMessage({ isCreatedByUser: true, depth: 2 }); + expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Prompt 3: '); + }); + + it('returns "Response N: " for AI messages with valid depth', () => { + const msg = makeMessage({ isCreatedByUser: false, depth: 0 }); + expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Response 1: '); + }); + + it('returns "Prompt: " for user messages without depth', () => { + const msg = makeMessage({ isCreatedByUser: true, depth: undefined }); + expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Prompt: '); + }); + + it('returns "Response: " for AI messages without depth', () => { + const msg = makeMessage({ isCreatedByUser: false, depth: undefined }); + expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Response: '); + }); + + it('omits number when depth is -1 (no "Prompt 0:" regression)', () => { + const msg = makeMessage({ isCreatedByUser: true, depth: -1 }); + expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Prompt: '); + }); + + it('omits number when depth is negative', () => { + const msg = makeMessage({ isCreatedByUser: false, depth: -5 }); + expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Response: '); + }); +}); diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts index 7197b6c2db..27dc063481 100644 --- a/client/src/utils/messages.ts +++ b/client/src/utils/messages.ts @@ -14,7 +14,6 @@ import type { } from 'librechat-data-provider'; import type { QueryClient } from '@tanstack/react-query'; import type { LocalizeFunction } from '~/common'; -import _ from 'lodash'; export const TEXT_KEY_DIVIDER = '|||'; @@ -185,12 +184,36 @@ export const clearMessagesCache = ( } }; +/** Returns a 1-based message number, or null if depth is absent or invalid. */ +const getMessageNumber = (message: TMessage): number | null => { + if (message.depth == null || message.depth < 0) { + return null; + } + return message.depth + 1; +}; + export const getMessageAriaLabel = (message: TMessage, localize: LocalizeFunction): string => { - return !_.isNil(message.depth) - ? localize('com_endpoint_message_new', { 0: message.depth + 1 }) + const number = getMessageNumber(message); + return number != null + ? localize('com_endpoint_message_new', { 0: number }) : localize('com_endpoint_message'); }; +/** + * Provides a screen-reader-only heading prefix distinguishing prompts from responses, + * with an optional 1-based turn number derived from message depth. + */ +export const getHeaderPrefixForScreenReader = ( + message: TMessage, + localize: LocalizeFunction, +): string => { + const number = getMessageNumber(message); + const suffix = number != null ? ` ${number}` : ''; + return message.isCreatedByUser + ? `${localize('com_ui_prompt')}${suffix}: ` + : `${localize('com_ui_response')}${suffix}: `; +}; + /** * Creates initial content parts for dual message display with agent-based grouping. * Sets up primary and added agent content parts with agentId for column rendering. From 676d297cb47f00e0c594ab384b316abc7d7af16a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 20 Mar 2026 16:53:26 -0400 Subject: [PATCH 088/111] =?UTF-8?q?=F0=9F=97=A3=EF=B8=8F=20a11y:=20Add=20S?= =?UTF-8?q?creen=20Reader=20Context=20to=20Conversation=20Date=20Group=20H?= =?UTF-8?q?eadings=20(#12340)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add screen reader-only context for convo date groupings Otherwise, the screen reader simply says something like "today" or "previous 7 days" without any other context, which is confusing (especially since this is a heading, so theoretically something you'd navigate to directly). Visually it's identical to before, but screen readers have added context now. * fix: move a11y key to com_a11y_* namespace and add DateLabel test Move screen-reader-only translation key from com_ui_* to com_a11y_* namespace where it belongs, and add test coverage to prevent silent accessibility regressions. --------- Co-authored-by: Dan Lew --- .../Conversations/Conversations.tsx | 4 ++ .../__tests__/DateLabel.test.tsx | 55 +++++++++++++++++++ client/src/locales/en/translation.json | 1 + 3 files changed, 60 insertions(+) create mode 100644 client/src/components/Conversations/__tests__/DateLabel.test.tsx diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx index c7eb4d53ef..f55af35f10 100644 --- a/client/src/components/Conversations/Conversations.tsx +++ b/client/src/components/Conversations/Conversations.tsx @@ -99,6 +99,9 @@ const DateLabel: FC<{ groupName: string; isFirst?: boolean }> = memo(({ groupNam const localize = useLocalize(); return (

@@ -394,4 +397,5 @@ const Conversations: FC = ({ ); }; +export { DateLabel }; export default memo(Conversations); diff --git a/client/src/components/Conversations/__tests__/DateLabel.test.tsx b/client/src/components/Conversations/__tests__/DateLabel.test.tsx new file mode 100644 index 0000000000..ccd9bc4126 --- /dev/null +++ b/client/src/components/Conversations/__tests__/DateLabel.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { DateLabel } from '../Conversations'; + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string, params?: Record) => { + const translations: Record = { + com_a11y_chats_date_section: `Chats from ${params?.date ?? ''}`, + com_ui_date_today: 'Today', + com_ui_date_yesterday: 'Yesterday', + com_ui_date_previous_7_days: 'Previous 7 days', + }; + return translations[key] ?? key; + }, +})); + +describe('DateLabel', () => { + it('provides accessible heading name via aria-label', () => { + render(); + expect(screen.getByRole('heading', { level: 2, name: 'Chats from Today' })).toBeInTheDocument(); + }); + + it('renders visible text as the localized group name', () => { + render(); + expect(screen.getByText('Today')).toBeInTheDocument(); + }); + + it('sets aria-label with the full accessible phrase', () => { + const { container } = render(); + const heading = container.querySelector('h2'); + expect(heading).toHaveAttribute('aria-label', 'Chats from Yesterday'); + }); + + it('uses raw groupName for unrecognized translation keys', () => { + render(); + expect( + screen.getByRole('heading', { level: 2, name: 'Chats from Unknown Group' }), + ).toBeInTheDocument(); + }); + + it('applies mt-0 for the first date header', () => { + const { container } = render(); + const heading = container.querySelector('h2'); + expect(heading).toHaveClass('mt-0'); + expect(heading).not.toHaveClass('mt-2'); + }); + + it('applies mt-2 for non-first date headers', () => { + const { container } = render(); + const heading = container.querySelector('h2'); + expect(heading).toHaveClass('mt-2'); + expect(heading).not.toHaveClass('mt-0'); + }); +}); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 9f641fdb16..67111586ff 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -2,6 +2,7 @@ "chat_direction_left_to_right": "Left to Right", "chat_direction_right_to_left": "Right to Left", "com_a11y_ai_composing": "The AI is still composing.", + "com_a11y_chats_date_section": "Chats from {{date}}", "com_a11y_end": "The AI has finished their reply.", "com_a11y_selected": "selected", "com_a11y_start": "The AI has started their reply.", From 365a0dc0f69a48d6dc669cade8bd2822958797b4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 20 Mar 2026 17:10:25 -0400 Subject: [PATCH 089/111] =?UTF-8?q?=F0=9F=A9=BA=20refactor:=20Surface=20De?= =?UTF-8?q?scriptive=20OCR=20Error=20Messages=20to=20Client=20(#12344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: pass along error message when OCR fails Right now, if OCR fails, it just says "Error processing file" which isn't very helpful. The `error.message` does has helpful information in it, but our filter wasn't including the right case to pass it along. Now it does! * fix: extract shared upload error filter, apply to images route The 'Unable to extract text from' error was only allowlisted in the files route but not the images route, which also calls processAgentFileUpload. Extract the duplicated error filter logic into a shared resolveUploadErrorMessage utility in packages/api so both routes stay in sync. --------- Co-authored-by: Dan Lew --- api/server/routes/files/files.js | 16 +------- api/server/routes/files/images.js | 12 +----- packages/api/src/utils/files.spec.ts | 55 +++++++++++++++++++++++++++- packages/api/src/utils/files.ts | 33 +++++++++++++++++ 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index 9290d1a7ed..fdb7768c3b 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -2,7 +2,7 @@ const fs = require('fs').promises; const express = require('express'); const { EnvVar } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); -const { verifyAgentUploadPermission } = require('@librechat/api'); +const { verifyAgentUploadPermission, resolveUploadErrorMessage } = require('@librechat/api'); const { Time, isUUID, @@ -394,21 +394,9 @@ router.post('/', async (req, res) => { return await processAgentFileUpload({ req, res, metadata }); } catch (error) { - let message = 'Error processing file'; + const message = resolveUploadErrorMessage(error); logger.error('[/files] Error processing file:', error); - if (error.message?.includes('file_ids')) { - message += ': ' + error.message; - } - - if ( - error.message?.includes('Invalid file format') || - error.message?.includes('No OCR result') || - error.message?.includes('exceeds token limit') - ) { - message = error.message; - } - try { await fs.unlink(req.file.path); cleanup = false; diff --git a/api/server/routes/files/images.js b/api/server/routes/files/images.js index 185ec7a671..d5d8f51193 100644 --- a/api/server/routes/files/images.js +++ b/api/server/routes/files/images.js @@ -2,7 +2,7 @@ const path = require('path'); const fs = require('fs').promises; const express = require('express'); const { logger } = require('@librechat/data-schemas'); -const { verifyAgentUploadPermission } = require('@librechat/api'); +const { verifyAgentUploadPermission, resolveUploadErrorMessage } = require('@librechat/api'); const { isAssistantsEndpoint } = require('librechat-data-provider'); const { processAgentFileUpload, @@ -43,15 +43,7 @@ router.post('/', async (req, res) => { // TODO: delete remote file if it exists logger.error('[/files/images] Error processing file:', error); - let message = 'Error processing file'; - - if ( - error.message?.includes('Invalid file format') || - error.message?.includes('No OCR result') || - error.message?.includes('exceeds token limit') - ) { - message = error.message; - } + const message = resolveUploadErrorMessage(error); try { const filepath = path.join( diff --git a/packages/api/src/utils/files.spec.ts b/packages/api/src/utils/files.spec.ts index db51d51e0f..2f1b1346aa 100644 --- a/packages/api/src/utils/files.spec.ts +++ b/packages/api/src/utils/files.spec.ts @@ -1,4 +1,4 @@ -import { sanitizeFilename } from './files'; +import { sanitizeFilename, resolveUploadErrorMessage } from './files'; jest.mock('node:crypto', () => { const actualModule = jest.requireActual('node:crypto'); @@ -52,6 +52,59 @@ describe('sanitizeFilename', () => { }); }); +describe('resolveUploadErrorMessage', () => { + test('returns default message for null error', () => { + expect(resolveUploadErrorMessage(null)).toBe('Error processing file'); + }); + + test('returns default message for undefined error', () => { + expect(resolveUploadErrorMessage(undefined)).toBe('Error processing file'); + }); + + test('returns default message when error has no message property', () => { + expect(resolveUploadErrorMessage({})).toBe('Error processing file'); + }); + + test('returns default message for unrecognized error', () => { + expect(resolveUploadErrorMessage({ message: 'ENOENT: no such file or directory' })).toBe( + 'Error processing file', + ); + }); + + test('prepends default message for file_ids errors', () => { + expect(resolveUploadErrorMessage({ message: 'max file_ids reached' })).toBe( + 'Error processing file: max file_ids reached', + ); + }); + + test('surfaces "Invalid file format" errors', () => { + expect(resolveUploadErrorMessage({ message: 'Invalid file format: .xyz' })).toBe( + 'Invalid file format: .xyz', + ); + }); + + test('surfaces "exceeds token limit" errors', () => { + expect(resolveUploadErrorMessage({ message: 'Content exceeds token limit' })).toBe( + 'Content exceeds token limit', + ); + }); + + test('surfaces "Unable to extract text from" errors', () => { + const msg = 'Unable to extract text from "doc.pdf". The document may be image-based.'; + expect(resolveUploadErrorMessage({ message: msg })).toBe(msg); + }); + + test('accepts a custom default message', () => { + expect(resolveUploadErrorMessage(null, 'Custom default')).toBe('Custom default'); + }); + + test('uses custom default in file_ids prepend', () => { + expect(resolveUploadErrorMessage({ message: 'file_ids limit' }, 'Upload failed')).toBe( + 'Upload failed: file_ids limit', + ); + }); +}); + describe('sanitizeFilename with real crypto', () => { // Temporarily unmock crypto for these tests beforeAll(() => { diff --git a/packages/api/src/utils/files.ts b/packages/api/src/utils/files.ts index 2fa3b62ab3..9e78d9289c 100644 --- a/packages/api/src/utils/files.ts +++ b/packages/api/src/utils/files.ts @@ -3,6 +3,39 @@ import crypto from 'node:crypto'; import { createReadStream } from 'fs'; import { readFile, stat } from 'fs/promises'; +const USER_FACING_UPLOAD_ERRORS = [ + 'Invalid file format', + 'exceeds token limit', + 'Unable to extract text from', +] as const; + +/** + * Resolves a user-facing error message from a file upload error. + * Returns the error's own message if it matches a known user-facing pattern, + * otherwise returns the default message. + */ +export function resolveUploadErrorMessage( + error: { message?: string } | null | undefined, + defaultMessage = 'Error processing file', +): string { + const errorMessage = error?.message; + if (!errorMessage) { + return defaultMessage; + } + + if (errorMessage.includes('file_ids')) { + return `${defaultMessage}: ${errorMessage}`; + } + + for (const fragment of USER_FACING_UPLOAD_ERRORS) { + if (errorMessage.includes(fragment)) { + return errorMessage; + } + } + + return defaultMessage; +} + /** * Sanitize a filename by removing any directory components, replacing non-alphanumeric characters * @param inputName From 01f19b503a993efbf956b42a2e5e0029d040812f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 20 Mar 2026 17:10:39 -0400 Subject: [PATCH 090/111] =?UTF-8?q?=F0=9F=9B=82=20fix:=20Gate=20MCP=20Quer?= =?UTF-8?q?ies=20Behind=20USE=20Permission=20to=20Prevent=20403=20Spam=20(?= =?UTF-8?q?#12345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: Gate MCP queries behind USE permission to prevent 403 spam Closes #12342 When `interface.mcpServers.use` is set to `false` in `librechat.yaml`, the frontend was still unconditionally fetching `/api/mcp/servers` on every app startup, window focus, and stale interval — producing continuous 403 "Insufficient permissions" log entries. Add `useHasAccess` permission checks to both `useMCPServersQuery` call sites (`useAppStartup` and `useMCPServerManager`) so the query is disabled when the user lacks `MCP_SERVERS.USE`, matching the guard pattern already used by MCP UI components. * fix: Lint and import order corrections * fix: Address review findings — gate permissions query, add tests - Gate `useGetAllEffectivePermissionsQuery` behind `canUseMcp` in `useMCPServerManager` for consistency (wasted request when MCP disabled, even though this endpoint doesn't 403) - Sort multi-line `librechat-data-provider` import shortest to longest - Restore intent comment on `useGetStartupConfig` call - Add `useAppStartup` test suite covering MCP permission gating: query suppression when USE denied, compound `enabled` conditions for tools query (servers loading, empty, no user) --- .../Config/__tests__/useAppStartup.spec.tsx | 123 ++++++++++++++++++ client/src/hooks/Config/useAppStartup.ts | 20 ++- client/src/hooks/MCP/useMCPServerManager.ts | 24 +++- 3 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 client/src/hooks/Config/__tests__/useAppStartup.spec.tsx diff --git a/client/src/hooks/Config/__tests__/useAppStartup.spec.tsx b/client/src/hooks/Config/__tests__/useAppStartup.spec.tsx new file mode 100644 index 0000000000..eef2795a76 --- /dev/null +++ b/client/src/hooks/Config/__tests__/useAppStartup.spec.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { RecoilRoot } from 'recoil'; +import { renderHook } from '@testing-library/react'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; +import type { TUser } from 'librechat-data-provider'; + +const mockUseHasAccess = jest.fn(); +const mockUseMCPServersQuery = jest.fn(); +const mockUseMCPToolsQuery = jest.fn(); + +jest.mock('~/hooks', () => ({ + useHasAccess: (args: unknown) => mockUseHasAccess(args), +})); + +jest.mock('~/data-provider', () => ({ + useMCPServersQuery: (config: unknown) => mockUseMCPServersQuery(config), + useMCPToolsQuery: (config: unknown) => mockUseMCPToolsQuery(config), +})); + +jest.mock('../useSpeechSettingsInit', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('~/utils/timestamps', () => ({ + cleanupTimestampedStorage: jest.fn(), +})); + +jest.mock('react-gtm-module', () => ({ + __esModule: true, + default: { initialize: jest.fn() }, +})); + +import useAppStartup from '../useAppStartup'; + +const mockUser = { + id: 'user-123', + username: 'testuser', + email: 'test@example.com', + name: 'Test User', + avatar: '', + role: 'USER', + provider: 'local', + emailVerified: true, + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', +} as TUser; + +const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +describe('useAppStartup — MCP permission gating', () => { + beforeEach(() => { + mockUseMCPServersQuery.mockReturnValue({ data: undefined, isLoading: false }); + mockUseMCPToolsQuery.mockReturnValue({ data: undefined, isLoading: false }); + }); + + it('checks the MCP_SERVERS.USE permission via useHasAccess', () => { + mockUseHasAccess.mockReturnValue(false); + + renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper }); + + expect(mockUseHasAccess).toHaveBeenCalledWith({ + permissionType: PermissionTypes.MCP_SERVERS, + permission: Permissions.USE, + }); + }); + + it('suppresses all MCP queries when user lacks MCP_SERVERS.USE', () => { + mockUseHasAccess.mockReturnValue(false); + + renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper }); + + expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: false }); + expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false }); + }); + + it('enables servers query and tools query when permission granted, servers loaded, and user present', () => { + mockUseHasAccess.mockReturnValue(true); + mockUseMCPServersQuery.mockReturnValue({ + data: { 'test-server': { url: 'http://test' } }, + isLoading: false, + }); + + renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper }); + + expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: true }); + expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: true }); + }); + + it('suppresses tools query when permission granted but user prop is undefined', () => { + mockUseHasAccess.mockReturnValue(true); + mockUseMCPServersQuery.mockReturnValue({ + data: { 'test-server': { url: 'http://test' } }, + isLoading: false, + }); + + renderHook(() => useAppStartup({ startupConfig: undefined, user: undefined }), { wrapper }); + + expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: true }); + expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false }); + }); + + it('suppresses tools query when permission granted but no servers loaded', () => { + mockUseHasAccess.mockReturnValue(true); + mockUseMCPServersQuery.mockReturnValue({ data: {}, isLoading: false }); + + renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper }); + + expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: true }); + expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false }); + }); + + it('suppresses tools query while servers are still loading', () => { + mockUseHasAccess.mockReturnValue(true); + mockUseMCPServersQuery.mockReturnValue({ data: undefined, isLoading: true }); + + renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper }); + + expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false }); + }); +}); diff --git a/client/src/hooks/Config/useAppStartup.ts b/client/src/hooks/Config/useAppStartup.ts index 52b4325eea..f40b283ee2 100644 --- a/client/src/hooks/Config/useAppStartup.ts +++ b/client/src/hooks/Config/useAppStartup.ts @@ -1,11 +1,12 @@ import { useEffect } from 'react'; import { useRecoilState } from 'recoil'; import TagManager from 'react-gtm-module'; -import { LocalStorageKeys } from 'librechat-data-provider'; +import { LocalStorageKeys, PermissionTypes, Permissions } from 'librechat-data-provider'; import type { TStartupConfig, TUser } from 'librechat-data-provider'; +import { useMCPToolsQuery, useMCPServersQuery } from '~/data-provider'; import { cleanupTimestampedStorage } from '~/utils/timestamps'; import useSpeechSettingsInit from './useSpeechSettingsInit'; -import { useMCPToolsQuery, useMCPServersQuery } from '~/data-provider'; +import { useHasAccess } from '~/hooks'; import store from '~/store'; export default function useAppStartup({ @@ -16,12 +17,23 @@ export default function useAppStartup({ user?: TUser; }) { const [defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset); + const canUseMcp = useHasAccess({ + permissionType: PermissionTypes.MCP_SERVERS, + permission: Permissions.USE, + }); useSpeechSettingsInit(!!user); - const { data: loadedServers, isLoading: serversLoading } = useMCPServersQuery(); + const { data: loadedServers, isLoading: serversLoading } = useMCPServersQuery({ + enabled: canUseMcp, + }); useMCPToolsQuery({ - enabled: !serversLoading && !!loadedServers && Object.keys(loadedServers).length > 0 && !!user, + enabled: + canUseMcp && + !serversLoading && + !!loadedServers && + Object.keys(loadedServers).length > 0 && + !!user, }); /** Clean up old localStorage entries on startup */ diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index af65ba4507..4ba1ff6278 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -2,7 +2,14 @@ import { useCallback, useState, useMemo, useRef, useEffect } from 'react'; import { useAtom } from 'jotai'; import { useToastContext } from '@librechat/client'; import { useQueryClient } from '@tanstack/react-query'; -import { Constants, QueryKeys, MCPOptions, ResourceType } from 'librechat-data-provider'; +import { + Constants, + QueryKeys, + MCPOptions, + Permissions, + ResourceType, + PermissionTypes, +} from 'librechat-data-provider'; import { useCancelMCPOAuthMutation, useUpdateUserPluginsMutation, @@ -11,7 +18,7 @@ import { } from 'librechat-data-provider/react-query'; import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider'; import type { ConfigFieldDetail } from '~/common'; -import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks'; +import { useLocalize, useHasAccess, useMCPSelect, useMCPConnectionStatus } from '~/hooks'; import { useGetStartupConfig, useMCPServersQuery } from '~/data-provider'; import { mcpServerInitStatesAtom, getServerInitState } from '~/store/mcp'; import type { MCPServerInitState } from '~/store/mcp'; @@ -35,12 +42,19 @@ export function useMCPServerManager({ const localize = useLocalize(); const queryClient = useQueryClient(); const { showToast } = useToastContext(); - const { data: startupConfig } = useGetStartupConfig(); // Keep for UI config only + /** Retained for `interface.mcpServers.placeholder` used by `placeholderText` below */ + const { data: startupConfig } = useGetStartupConfig(); + const canUseMcp = useHasAccess({ + permissionType: PermissionTypes.MCP_SERVERS, + permission: Permissions.USE, + }); - const { data: loadedServers, isLoading } = useMCPServersQuery(); + const { data: loadedServers, isLoading } = useMCPServersQuery({ enabled: canUseMcp }); // Fetch effective permissions for all MCP servers - const { data: permissionsMap } = useGetAllEffectivePermissionsQuery(ResourceType.MCPSERVER); + const { data: permissionsMap } = useGetAllEffectivePermissionsQuery(ResourceType.MCPSERVER, { + enabled: canUseMcp, + }); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [selectedToolForConfig, setSelectedToolForConfig] = useState(null); From 0736ff26686e911c9785a237c63a799db1813f0b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 20 Mar 2026 18:01:00 -0400 Subject: [PATCH 091/111] =?UTF-8?q?=E2=9C=A8=20v0.8.4=20(#12339)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔖 chore: Bump version to v0.8.4 - App version: v0.8.4-rc1 → v0.8.4 - @librechat/api: 1.7.26 → 1.7.27 - @librechat/client: 0.4.55 → 0.4.56 - librechat-data-provider: 0.8.400 → 0.8.401 - @librechat/data-schemas: 0.0.39 → 0.0.40 * chore: bun.lock file bumps --- Dockerfile | 2 +- Dockerfile.multi | 2 +- api/package.json | 2 +- bun.lock | 1358 +++++++++++++++++++++----- client/jest.config.cjs | 2 +- client/package.json | 2 +- e2e/jestSetup.js | 2 +- helm/librechat/Chart.yaml | 4 +- package-lock.json | 16 +- package.json | 2 +- packages/api/package.json | 2 +- packages/client/package.json | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/config.ts | 2 +- packages/data-schemas/package.json | 2 +- 15 files changed, 1115 insertions(+), 287 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3c4963f970..19d275eb31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# v0.8.4-rc1 +# v0.8.4 # Base node image FROM node:20-alpine AS node diff --git a/Dockerfile.multi b/Dockerfile.multi index bc4203f265..bf5570f386 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -1,5 +1,5 @@ # Dockerfile.multi -# v0.8.4-rc1 +# v0.8.4 # Set configurable max-old-space-size with default ARG NODE_MAX_OLD_SPACE_SIZE=6144 diff --git a/api/package.json b/api/package.json index 4416acd1d8..aea98b3f8d 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/backend", - "version": "v0.8.4-rc1", + "version": "v0.8.4", "description": "", "scripts": { "start": "echo 'please run this from the root directory'", diff --git a/bun.lock b/bun.lock index f6e3228519..fb1ec00840 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "api": { "name": "@librechat/backend", - "version": "0.8.4-rc1", + "version": "0.8.4", "dependencies": { "@anthropic-ai/vertex-sdk": "^0.14.3", "@aws-sdk/client-bedrock-runtime": "^3.980.0", @@ -49,7 +49,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.56", + "@librechat/agents": "^3.1.57", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -118,6 +118,7 @@ "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "yauzl": "^3.2.1", "zod": "^3.22.4", }, "devDependencies": { @@ -129,13 +130,13 @@ }, "client": { "name": "@librechat/frontend", - "version": "0.8.4-rc1", + "version": "0.8.4", "dependencies": { "@ariakit/react": "^0.4.15", "@ariakit/react-core": "^0.4.17", "@codesandbox/sandpack-react": "^2.19.10", - "@dicebear/collection": "^9.2.2", - "@dicebear/core": "^9.2.2", + "@dicebear/collection": "^9.4.1", + "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", @@ -263,7 +264,7 @@ }, "packages/api": { "name": "@librechat/api", - "version": "1.7.26", + "version": "1.7.27", "devDependencies": { "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", @@ -284,8 +285,10 @@ "@types/node-fetch": "^2.6.13", "@types/react": "^18.2.18", "@types/winston": "^2.4.4", + "@types/yauzl": "^2.10.3", "jest": "^30.2.0", "jest-junit": "^16.0.0", + "jszip": "^3.10.1", "librechat-data-provider": "*", "mammoth": "^1.11.0", "mongodb": "^6.14.2", @@ -296,6 +299,7 @@ "ts-node": "^10.9.2", "typescript": "^5.0.4", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "yauzl": "^3.2.1", }, "peerDependencies": { "@anthropic-ai/vertex-sdk": "^0.14.3", @@ -307,7 +311,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.56", + "@librechat/agents": "^3.1.57", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", @@ -335,12 +339,13 @@ "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "undici": "^7.24.1", + "yauzl": "^3.2.1", "zod": "^3.22.4", }, }, "packages/client": { "name": "@librechat/client", - "version": "0.4.55", + "version": "0.4.56", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -381,8 +386,8 @@ "peerDependencies": { "@ariakit/react": "^0.4.16", "@ariakit/react-core": "^0.4.17", - "@dicebear/collection": "^9.2.2", - "@dicebear/core": "^9.2.2", + "@dicebear/collection": "^9.4.1", + "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "1.0.2", @@ -428,7 +433,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.400", + "version": "0.8.401", "dependencies": { "axios": "^1.13.5", "dayjs": "^1.11.13", @@ -465,7 +470,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.39", + "version": "0.0.40", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^29.0.0", @@ -503,9 +508,8 @@ "overrides": { "@anthropic-ai/sdk": "0.73.0", "@hono/node-server": "^1.19.10", - "axios": "1.12.1", "elliptic": "^6.6.1", - "fast-xml-parser": "5.3.8", + "fast-xml-parser": "5.5.7", "form-data": "^4.0.4", "hono": "^4.12.4", "katex": "^0.16.21", @@ -555,7 +559,7 @@ "@aws-sdk/client-bedrock-agent-runtime": ["@aws-sdk/client-bedrock-agent-runtime@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/credential-provider-node": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/eventstream-serde-browser": "^4.2.4", "@smithy/eventstream-serde-config-resolver": "^4.3.4", "@smithy/eventstream-serde-node": "^4.2.4", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k2UeG/+Ka74jztHDzYNrpNLDSsMCst+ph3+e7uAX5Jmo40tVKa+sVu4DkV3BIXuktc6jqM1ewtfPNug79kN6JQ=="], - "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1004.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.18", "@aws-sdk/eventstream-handler-node": "^3.972.10", "@aws-sdk/middleware-eventstream": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/middleware-websocket": "^3.972.12", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw=="], + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1013.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.22", "@aws-sdk/credential-provider-node": "^3.972.23", "@aws-sdk/eventstream-handler-node": "^3.972.11", "@aws-sdk/middleware-eventstream": "^3.972.8", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/middleware-websocket": "^3.972.13", "@aws-sdk/region-config-resolver": "^3.972.8", "@aws-sdk/token-providers": "3.1013.0", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.9", "@smithy/config-resolver": "^4.4.11", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.26", "@smithy/middleware-retry": "^4.4.43", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.42", "@smithy/util-defaults-mode-node": "^4.2.45", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-LU80q1avpBwQ0eVAGbQpPApdVY4vcdBEIycY5iaznI10mdabeG83nrFySJrZ8knX7G6hl5d5KIOSjcpnolMKSA=="], "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/client-sso-oidc": "3.623.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-kGYnTzXTMGdjko5+GZ1PvWvfXA7quiOp5iMo5gbh5b55pzIdc918MHN0pvaqplVGWYlaFJF4YzxUT5Nbxd7Xeg=="], @@ -567,7 +571,7 @@ "@aws-sdk/client-sso-oidc": ["@aws-sdk/client-sso-oidc@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-lMFEXCa6ES/FGV7hpyrppT1PiAkqQb51AbG0zVU3TIgI2IO4XX02uzMUXImRSRqRpGymRCbJCaCs9LtKvS/37Q=="], - "@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], + "@aws-sdk/core": ["@aws-sdk/core@3.973.22", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.14", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-lY6g5L95jBNgOUitUhfV2N/W+i08jHEl3xuLODYSQH5Sf50V+LkVYBSyZRLtv2RyuXZXiV7yQ+acpswK1tlrOA=="], "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw=="], @@ -579,9 +583,9 @@ "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.623.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-kvXA1SwGneqGzFwRZNpESitnmaENHGFFuuTvgGwtMe7mzXWuA/LkXdbiHmdyAzOo0iByKTCD8uetuwh3CXy4Pw=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-u33CO9zeNznlVSg9tWTCRYxaGkqr1ufU6qeClpmzAabXZa8RZxQoVXxL5T53oZJFzQYj+FImORCSsi7H7B77gQ=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.18", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-ini": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.23", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.20", "@aws-sdk/credential-provider-http": "^3.972.22", "@aws-sdk/credential-provider-ini": "^3.972.22", "@aws-sdk/credential-provider-process": "^3.972.20", "@aws-sdk/credential-provider-sso": "^3.972.22", "@aws-sdk/credential-provider-web-identity": "^3.972.22", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-U8tyLbLOZItuVWTH0ay9gWo4xMqZwqQbg1oMzdU4FQSkTpqXemm4X0uoKBR6llqAStgBp30ziKFJHTA43l4qMw=="], "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.620.1", "", { "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg=="], @@ -591,57 +595,57 @@ "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.623.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.623.0", "@aws-sdk/client-sso": "3.623.0", "@aws-sdk/credential-provider-cognito-identity": "3.623.0", "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-ini": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-abtlH1hkVWAkzuOX79Q47l0ztWOV2Q7l7J4JwQgzEQm7+zCk5iUAiwqKyDzr+ByCyo4I3IWFjy+e1gBdL7rXQQ=="], - "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA=="], + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA=="], "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA=="], - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g=="], + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ=="], "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ=="], "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.973.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/crc64-nvme": "^3.972.4", "@aws-sdk/types": "^3.973.5", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7CH2jcGmkvkHc5Buz9IGbdjq1729AAlgYJiAvGq7qhCHqYleCsriWdSnmsqWTwdAfXHMT+pkxX3w6v5tJNcSug=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="], "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA=="], "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw=="], "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.8", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-HQu8QoqGZZTvg0Spl9H39QTsSMFwgu+8yz/QGKndXFLk9FZMiCiIgBCVlTVKMDvVbgqIzD9ig+/HmXsIL2Rb+g=="], - "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-format-url": "^3.972.7", "@smithy/eventstream-codec": "^4.2.11", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q=="], + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.13", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", "@smithy/eventstream-codec": "^4.2.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.12", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.22", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/region-config-resolver": "^3.972.8", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.9", "@smithy/config-resolver": "^4.4.11", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.26", "@smithy/middleware-retry": "^4.4.43", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.42", "@smithy/util-defaults-mode-node": "^4.2.45", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KLdQGJPSm98uLINolQ0Tol8OAbk7g0Y7zplHJ1K83vbMIH13aoCvR6Tho66xueW4l4aZlEgVGLWBnD8ifUMsGQ=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.11", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw=="], "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.758.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-format-url": "3.734.0", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dVyItwu/J1InfJBbCPpHRV9jrsBfI7L0RlDGyS3x/xqBwnm5qpvgNZQasQiyqIl+WJB4f5rZRZHgHuwftqINbA=="], "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.6", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.18", "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1004.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1013.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IL1c54UvbuERrs9oLm5rvkzMciwhhpn1FL0SlC3XUMoLlFhdBsWJgQKK8O5fsQLxbFVqjbjFx9OBkrn44X9PHw=="], - "@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + "@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="], "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="], "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-TxZMVm8V4aR/QkW9/NhujvYpPZjUYqzLwSge5imKZbWFR806NP7RMwc5ilVuHF/bMOln/cVHkl42kATElWBvNw=="], "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.568.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.4", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.9", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-jeFqqp8KD/P5O+qeKxyGeu7WEVIZFNprnkaDjGmBOjwxYwafCBhpxTgV1TlW6L8e76Vh/siNylNmN/OmSIFBUQ=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.14", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.6", "tslib": "^2.6.2" } }, "sha512-G/Yd8Bnnyh8QrqLf8jWJbixEnScUFW24e/wOBGYdw1Cl4r80KX/DvHyM2GVZ2vTp7J4gTEr8IXJlTadA8+UfuQ=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="], @@ -683,13 +687,13 @@ "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], - "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], @@ -707,7 +711,7 @@ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], @@ -727,9 +731,9 @@ "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], @@ -909,11 +913,11 @@ "@babel/runtime": ["@babel/runtime@7.26.10", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="], - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], @@ -1067,69 +1071,71 @@ "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], - "@dicebear/adventurer": ["@dicebear/adventurer@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA=="], + "@dicebear/adventurer": ["@dicebear/adventurer@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-jqYp834ZmGDA9HBBDQAdgF1O2UTCwHF4vVrktXWa2Dppp1JczPL5HnVOWsjtrLmXNn61Wd6OLmBb2e6rhzp3ig=="], - "@dicebear/adventurer-neutral": ["@dicebear/adventurer-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg=="], + "@dicebear/adventurer-neutral": ["@dicebear/adventurer-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-5xgkG/mNL4j3Q4SJGQLBU/KnU90tng8Ze5ofThD+55wi0oeY/nSAUowg6UFCmHrktjifj/MEx3CQqbpcPWtfIA=="], - "@dicebear/avataaars": ["@dicebear/avataaars@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-QKNBtA/1QGEzR+JjS4XQyrFHYGbzdOp0oa6gjhGhUDrMegDFS8uyjdRfDQsFTebVkyLWjgBQKZEiDqKqHptB6A=="], + "@dicebear/avataaars": ["@dicebear/avataaars@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-3x9jKFkOkFSPmpTbt9xvhiU2E1GX7beCSsX0tXRUShj8x6+5Ks9yBRT1VlkySbnXrZ/GglADGg7vJ/D2uIx1Yw=="], - "@dicebear/avataaars-neutral": ["@dicebear/avataaars-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-HtBvA7elRv50QTOOsBdtYB1GVimCpGEDlDgWsu1snL5Z3d1+3dIESoXQd3mXVvKTVT8Z9ciA4TEaF09WfxDjAA=="], + "@dicebear/avataaars-neutral": ["@dicebear/avataaars-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-/eNrp0YCNJRwQXqOloLm1+3Ss2C+pMpUQIGkbEnGsP1UK+13Ge80ggDDof1HpdqvG9HAZcKa7hnbG/0HSwyDSw=="], - "@dicebear/big-ears": ["@dicebear/big-ears@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-U33tbh7Io6wG6ViUMN5fkWPER7hPKMaPPaYgafaYQlCT4E7QPKF2u8X1XGag3jCKm0uf4SLXfuZ8v+YONcHmNQ=="], + "@dicebear/big-ears": ["@dicebear/big-ears@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-mNfz3ppNA7UBq0IO3nXCiV5pFPG7c1DfzRB0foNU2Wo1XXT8FIcSY2BvDlYqorZTOUOz7dHb0vx06hqvG0HP5w=="], - "@dicebear/big-ears-neutral": ["@dicebear/big-ears-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-pPjYu80zMFl43A9sa5+tAKPkhp4n9nd7eN878IOrA1HAowh/XePh5JN8PTkNFS9eM+rnN9m8WX08XYFe30kLYw=="], + "@dicebear/big-ears-neutral": ["@dicebear/big-ears-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-M8Ozmzza4eY4hpLOYULgJxMYmBA0CsBnrE15/xw6LZkEREXnrX5z0NJsf8hUfdyF6BWZ+RBgzoiav32DAC5zcg=="], - "@dicebear/big-smile": ["@dicebear/big-smile@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-zeEfXOOXy7j9tfkPLzfQdLBPyQsctBetTdEfKRArc1k3RUliNPxfJG9j88+cXQC6GXrVW2pcT2X50NSPtugCFQ=="], + "@dicebear/big-smile": ["@dicebear/big-smile@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-hmT5i7rcPPhStjZyg28pbIhdTnnMBzK3RObI0vKCpY30EFrzaPkkdDL6Ck5fAFBdvDIW1EpOJkenyR0XPmhgbQ=="], - "@dicebear/bottts": ["@dicebear/bottts@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-4CTqrnVg+NQm6lZ4UuCJish8gGWe8EqSJrzvHQRO5TEyAKjYxbTdVqejpkycG1xkawha4FfxsYgtlSx7UwoVMw=="], + "@dicebear/bottts": ["@dicebear/bottts@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-tsx+dII7EFUCVA8URj66G1GqORCCVduCAx4dY2prEY2IeFianVpkntXuFsWZ9BBGx1NZFndvDith5oTwKMQPbQ=="], - "@dicebear/bottts-neutral": ["@dicebear/bottts-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-eMVdofdD/udHsKIaeWEXShDRtiwk7vp4FjY7l0f79vIzfhkIsXKEhPcnvHKOl/yoArlDVS3Uhgjj0crWTO9RJA=="], + "@dicebear/bottts-neutral": ["@dicebear/bottts-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-kFNwWt6j+gzZ5n5Pz7WVwePubREAQOF8ZwWA9ztwVYDVMLnOChWbAofy5FED4j5md2MXFH2EgLCFCMr5K2BmIA=="], - "@dicebear/collection": ["@dicebear/collection@9.2.4", "", { "dependencies": { "@dicebear/adventurer": "9.2.4", "@dicebear/adventurer-neutral": "9.2.4", "@dicebear/avataaars": "9.2.4", "@dicebear/avataaars-neutral": "9.2.4", "@dicebear/big-ears": "9.2.4", "@dicebear/big-ears-neutral": "9.2.4", "@dicebear/big-smile": "9.2.4", "@dicebear/bottts": "9.2.4", "@dicebear/bottts-neutral": "9.2.4", "@dicebear/croodles": "9.2.4", "@dicebear/croodles-neutral": "9.2.4", "@dicebear/dylan": "9.2.4", "@dicebear/fun-emoji": "9.2.4", "@dicebear/glass": "9.2.4", "@dicebear/icons": "9.2.4", "@dicebear/identicon": "9.2.4", "@dicebear/initials": "9.2.4", "@dicebear/lorelei": "9.2.4", "@dicebear/lorelei-neutral": "9.2.4", "@dicebear/micah": "9.2.4", "@dicebear/miniavs": "9.2.4", "@dicebear/notionists": "9.2.4", "@dicebear/notionists-neutral": "9.2.4", "@dicebear/open-peeps": "9.2.4", "@dicebear/personas": "9.2.4", "@dicebear/pixel-art": "9.2.4", "@dicebear/pixel-art-neutral": "9.2.4", "@dicebear/rings": "9.2.4", "@dicebear/shapes": "9.2.4", "@dicebear/thumbs": "9.2.4" }, "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-I1wCUp0yu5qSIeMQHmDYXQIXKkKjcja/SYBxppPkYFXpR2alxb0k9/swFDdMbkY6a1c9AT1kI1y+Pg6ywQ2rTA=="], + "@dicebear/collection": ["@dicebear/collection@9.4.2", "", { "dependencies": { "@dicebear/adventurer": "9.4.2", "@dicebear/adventurer-neutral": "9.4.2", "@dicebear/avataaars": "9.4.2", "@dicebear/avataaars-neutral": "9.4.2", "@dicebear/big-ears": "9.4.2", "@dicebear/big-ears-neutral": "9.4.2", "@dicebear/big-smile": "9.4.2", "@dicebear/bottts": "9.4.2", "@dicebear/bottts-neutral": "9.4.2", "@dicebear/croodles": "9.4.2", "@dicebear/croodles-neutral": "9.4.2", "@dicebear/dylan": "9.4.2", "@dicebear/fun-emoji": "9.4.2", "@dicebear/glass": "9.4.2", "@dicebear/icons": "9.4.2", "@dicebear/identicon": "9.4.2", "@dicebear/initials": "9.4.2", "@dicebear/lorelei": "9.4.2", "@dicebear/lorelei-neutral": "9.4.2", "@dicebear/micah": "9.4.2", "@dicebear/miniavs": "9.4.2", "@dicebear/notionists": "9.4.2", "@dicebear/notionists-neutral": "9.4.2", "@dicebear/open-peeps": "9.4.2", "@dicebear/personas": "9.4.2", "@dicebear/pixel-art": "9.4.2", "@dicebear/pixel-art-neutral": "9.4.2", "@dicebear/rings": "9.4.2", "@dicebear/shapes": "9.4.2", "@dicebear/thumbs": "9.4.2", "@dicebear/toon-head": "9.4.2" }, "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-KArubv7if8H7j9sIfpDK2hJJqrdNVR5zMPAMOSpIU2JPyXx8TC9o5wsmXb8il5wOHgaS9Q/cla7jUNIiDD7Gsg=="], - "@dicebear/core": ["@dicebear/core@9.2.4", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-hz6zArEcUwkZzGOSJkWICrvqnEZY7BKeiq9rqKzVJIc1tRVv0MkR0FGvIxSvXiK9TTIgKwu656xCWAGAl6oh+w=="], + "@dicebear/core": ["@dicebear/core@9.4.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w=="], - "@dicebear/croodles": ["@dicebear/croodles@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-CqT0NgVfm+5kd+VnjGY4WECNFeOrj5p7GCPTSEA7tCuN72dMQOX47P9KioD3wbExXYrIlJgOcxNrQeb/FMGc3A=="], + "@dicebear/croodles": ["@dicebear/croodles@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-6VoO0JviIf7dKKMBTL/SMXxWhnXHaZuzufX90G0nXxS77ELG1YkGNMaZzawizN4C09Gbya2gJkozqrWiJN/aGw=="], - "@dicebear/croodles-neutral": ["@dicebear/croodles-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-8vAS9lIEKffSUVx256GSRAlisB8oMX38UcPWw72venO/nitLVsyZ6hZ3V7eBdII0Onrjqw1RDndslQODbVcpTw=="], + "@dicebear/croodles-neutral": ["@dicebear/croodles-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-oG5IeUdtiYshQ89gkAVcl5w3xAEi5UZX2fTzIyelpBPCG176l7VuuFzlxi2umnB3E6LVHYy06DXvUo/p+rXB2Q=="], - "@dicebear/dylan": ["@dicebear/dylan@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-tiih1358djAq0jDDzmW3N3S4C3ynC2yn4hhlTAq/MaUAQtAi47QxdHdFGdxH0HBMZKqA4ThLdVk3yVgN4xsukg=="], + "@dicebear/dylan": ["@dicebear/dylan@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-1vQvRu9x9DrwFxhFaIU2rf0EUL04yDTbAt7fHyAjM0mEsKzTD4mRNf95tCRuavCoW6W48u7A/OY6jyIub6kxLQ=="], - "@dicebear/fun-emoji": ["@dicebear/fun-emoji@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-Od729skczse1HvHekgEFv+mSuJKMC4sl5hENGi/izYNe6DZDqJrrD0trkGT/IVh/SLXUFbq1ZFY9I2LoUGzFZg=="], + "@dicebear/fun-emoji": ["@dicebear/fun-emoji@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-kqB6LPkdYCdEU/mwbyz34xLzoNUKL6ARcoo3fr5ASq9D6ZE07qIKybC3xv5+CPz7VmspJ1Q3c/VVWVMDRP7Twg=="], - "@dicebear/glass": ["@dicebear/glass@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-5lxbJode1t99eoIIgW0iwZMoZU4jNMJv/6vbsgYUhAslYFX5zP0jVRscksFuo89TTtS7YKqRqZAL3eNhz4bTDw=="], + "@dicebear/glass": ["@dicebear/glass@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-z5qUogHQ1b6UJ2zCqT848mU2U9DKbVDhiX6GPDjD7tYLisCCJVisH9p6WyNdHvflUd4SHkA6gRqVJIh2v2HnTA=="], - "@dicebear/icons": ["@dicebear/icons@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-bRsK1qj8u9Z76xs8XhXlgVr/oHh68tsHTJ/1xtkX9DeTQTSamo2tS26+r231IHu+oW3mePtFnwzdG9LqEPRd4A=="], + "@dicebear/icons": ["@dicebear/icons@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-QSMMz0NA03ypSGhXC8HQX8FSj8lYT+/5yqH+/N03OH2IjL0q7wwGZ7nqsrtlRp76O5WqMTwGfSbTUUYPjFr+Xw=="], - "@dicebear/identicon": ["@dicebear/identicon@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-R9nw/E8fbu9HltHOqI9iL/o9i7zM+2QauXWMreQyERc39oGR9qXiwgBxsfYGcIS4C85xPyuL5B3I2RXrLBlJPg=="], + "@dicebear/identicon": ["@dicebear/identicon@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-JVDSmZsv11mSWqwAktK5x9Bslht2xY3TFUn8xzu6slAYe1Z7hEXZ76eb+UJ6F4qEzdwZ7xPWzAS6Nb0Y3A0pww=="], - "@dicebear/initials": ["@dicebear/initials@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-4SzHG5WoQZl1TGcpEZR4bdsSkUVqwNQCOwWSPAoBJa3BNxbVsvL08LF7I97BMgrCoknWZjQHUYt05amwTPTKtg=="], + "@dicebear/initials": ["@dicebear/initials@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-yePuIUasmwtl9IrtB6rEzE/zb5fImKP/neW0CdcTC2MwLgMuP1GLHEGRgg1zI8exIh+PMv1YdLGyyUuRTE2Qpw=="], - "@dicebear/lorelei": ["@dicebear/lorelei@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-eS4mPYUgDpo89HvyFAx/kgqSSKh8W4zlUA8QJeIUCWTB0WpQmeqkSgIyUJjGDYSrIujWi+zEhhckksM5EwW0Dg=="], + "@dicebear/lorelei": ["@dicebear/lorelei@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-YMv6vnriW6VLFDsreKuOnUFFno6SRe7+7X7R7zPY0rZ+MaHX9V3jcioIG+1PSjIHEDfOLUHpr5vd1JBWv8y7UA=="], - "@dicebear/lorelei-neutral": ["@dicebear/lorelei-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-bWq2/GonbcJULtT+B/MGcM2UnA7kBQoH+INw8/oW83WI3GNTZ6qEwe3/W4QnCgtSOhUsuwuiSULguAFyvtkOZQ=="], + "@dicebear/lorelei-neutral": ["@dicebear/lorelei-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-yspanTthA5vh6iCdeLzn6xZ4yYMYRcfcxblcgSvHTF1ut0bjAXtw5SXzZ6aJTrJWiHkzYOQuTOR6GVYiW80Q7w=="], - "@dicebear/micah": ["@dicebear/micah@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-XNWJ8Mx+pncIV8Ye0XYc/VkMiax8kTxcP3hLTC5vmELQyMSLXzg/9SdpI+W/tCQghtPZRYTT3JdY9oU9IUlP2g=="], + "@dicebear/micah": ["@dicebear/micah@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-e4D3W/OlChSsLo7Llwsy0J18vk0azJqF/uFoY+EKACCNHBc1HGNsqVvu2CTf+OWOA8wTyAK6UkjBN5p01r7D+g=="], - "@dicebear/miniavs": ["@dicebear/miniavs@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-k7IYTAHE/4jSO6boMBRrNlqPT3bh7PLFM1atfe0nOeCDwmz/qJUBP3HdONajbf3fmo8f2IZYhELrNWTOE7Ox3Q=="], + "@dicebear/miniavs": ["@dicebear/miniavs@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-wLwyFNNUnDRd3BbhSBhXR0XEpX8sG0/xDA5M/OkDoapLqZnnI48YLUSDd2N5QTAVMmcSEuZOYxkcnj7WW79vlg=="], - "@dicebear/notionists": ["@dicebear/notionists@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-zcvpAJ93EfC0xQffaPZQuJPShwPhnu9aTcoPsaYGmw0oEDLcv2XYmDhUUdX84QYCn6LtCZH053rHLVazRW+OGw=="], + "@dicebear/notionists": ["@dicebear/notionists@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-ZCySq+nxcD/x4xyYgytcj2N9uY3gxrL+qpnmOdp2BdA221KacVrxlsUPpIgEMqxS2rMmBQXfxg129Pzn4ycIpA=="], - "@dicebear/notionists-neutral": ["@dicebear/notionists-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-fskWzBVxQzJhCKqY24DGZbYHSBaauoRa1DgXM7+7xBuksH7mfbTmZTvnUAsAqJYBkla8IPb4ERKduDWtlWYYjQ=="], + "@dicebear/notionists-neutral": ["@dicebear/notionists-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-AyD9kEfVxQUwDGf4Op059gVmYIOAkTKg3dtE9h9mEKP7zl/kMy5B67BFFOo7sB0mXCjzAegZ6ekGU02E8+hIHw=="], - "@dicebear/open-peeps": ["@dicebear/open-peeps@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-s6nwdjXFsplqEI7imlsel4Gt6kFVJm6YIgtZSpry0UdwDoxUUudei5bn957j9lXwVpVUcRjJW+TuEKztYjXkKQ=="], + "@dicebear/open-peeps": ["@dicebear/open-peeps@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-i01tLgtp2g937T81sVeAOVlqsCtiTck/Kw20g7hN80+7xrXjOUepz2HPLy3HeiMjwjMGRy5o54kSd0/8Ht4Dqg=="], - "@dicebear/personas": ["@dicebear/personas@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-JNim8RfZYwb0MfxW6DLVfvreCFIevQg+V225Xe5tDfbFgbcYEp4OU/KaiqqO2476OBjCw7i7/8USbv2acBhjwA=="], + "@dicebear/personas": ["@dicebear/personas@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-NJlkvI5F5gugt6t2+7QrYNTwQC7+4IQZS3vG0dYk2BncxOHax0BuLovdSdiAesTL4ZkytFYIydWmKmV2/xcUwg=="], - "@dicebear/pixel-art": ["@dicebear/pixel-art@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-4Ao45asieswUdlCTBZqcoF/0zHR3OWUWB0Mvhlu9b1Fbc6IlPBiOfx2vsp6bnVGVnMag58tJLecx2omeXdECBQ=="], + "@dicebear/pixel-art": ["@dicebear/pixel-art@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-peHf7oKICDgBZ8dUyj+txPnS7VZEWgvKE+xW4mNQqBt6dYZIjmva2shOVHn0b1JU+FDxMx3uIkWVixKdUq4WGg=="], - "@dicebear/pixel-art-neutral": ["@dicebear/pixel-art-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-ZITPLD1cPN4GjKkhWi80s7e5dcbXy34ijWlvmxbc4eb/V7fZSsyRa9EDUW3QStpo+xrCJLcLR+3RBE5iz0PC/A=="], + "@dicebear/pixel-art-neutral": ["@dicebear/pixel-art-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-9e9Lz554uQvWaXV2P17ss+hPa6rTyuAKBtB8zk8ECjHiZzIl61N/KcTVLZ4dILVZwj7gYriaLo16QEqvL2GJCg=="], - "@dicebear/rings": ["@dicebear/rings@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-teZxELYyV2ogzgb5Mvtn/rHptT0HXo9SjUGS4A52mOwhIdHSGGU71MqA1YUzfae9yJThsw6K7Z9kzuY2LlZZHA=="], + "@dicebear/rings": ["@dicebear/rings@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-Pc3ymWrRDQPJFNrbbLt7RJrzGvUuuxUiDkrfLhoVE+B6mZWEL1PC78DPbS1yUWYLErJOpJuM2GSwXmTbVjWf+g=="], - "@dicebear/shapes": ["@dicebear/shapes@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-MhK9ZdFm1wUnH4zWeKPRMZ98UyApolf5OLzhCywfu38tRN6RVbwtBRHc/42ZwoN1JU1JgXr7hzjYucMqISHtbA=="], + "@dicebear/shapes": ["@dicebear/shapes@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-AFL6jAaiLztvcqyq+ds+lWZu6Vbp3PlGWhJeJRm842jxtiluJpl6r4f6nUXP2fdMz7MNpDzXfLooQK9E04NbUQ=="], - "@dicebear/thumbs": ["@dicebear/thumbs@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-EL4sMqv9p2+1Xy3d8e8UxyeKZV2+cgt3X2x2RTRzEOIIhobtkL8u6lJxmJbiGbpVtVALmrt5e7gjmwqpryYDpg=="], + "@dicebear/thumbs": ["@dicebear/thumbs@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-ccWvDBqbkWS5uzHbsg5L6uML6vBfX7jT3J3jHCQksvz8haHItxTK02w+6e1UavZUsvza4lG5X/XY3eji3siJ4Q=="], + + "@dicebear/toon-head": ["@dicebear/toon-head@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-lwFeSXyAnaKnCfMt9TiJwnD1cXQUGkey/0h6i/+4TVHVMCz5/Ri5u1ynovPNHy1SnBf858QwoXHkxilGLwQX/g=="], "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -1487,7 +1493,7 @@ "@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="], - "@librechat/agents": ["@librechat/agents@3.1.56", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.980.0", "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.80", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "@scarf/scarf": "^1.4.0", "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "okapibm25": "^1.4.1", "openai": "5.8.2" } }, "sha512-HJJwRnLM4XKpTWB4/wPDJR+iegyKBVUwqj7A8QHqzEcHzjKJDTr3wBPxZVH1tagGr6/mbbnErOJ14cH1OSNmpA=="], + "@librechat/agents": ["@librechat/agents@3.1.62", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.1013.0", "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.80", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "@scarf/scarf": "^1.4.0", "ai-tokenizer": "^1.0.6", "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "okapibm25": "^1.4.1", "openai": "5.8.2" } }, "sha512-QBZlJ4C89GmBg9w2qoWOWl1Y1xiRypUtIMBsL6eLPIsdbKHJ+GYO+076rfSD+tMqZB5ZbrxqPWOh+gxEXK1coQ=="], "@librechat/api": ["@librechat/api@workspace:packages/api"], @@ -1839,10 +1845,6 @@ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.37.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], @@ -1879,75 +1881,75 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="], "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="], - "@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], + "@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@3.2.0", "", { "dependencies": { "@smithy/node-config-provider": "^3.1.4", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "tslib": "^2.6.2" } }, "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA=="], - "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="], "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.12", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A=="], + "@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="], "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="], "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], "@smithy/md5-js": ["@smithy/md5-js@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.40", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/service-error-classification": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="], "@smithy/property-provider": ["@smithy/property-provider@3.1.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="], - "@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + "@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], - "@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], + "@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="], "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], @@ -1959,19 +1961,19 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.39", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.42", "", { "dependencies": { "@smithy/config-resolver": "^4.4.10", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="], "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="], - "@smithy/util-retry": ["@smithy/util-retry@4.2.11", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw=="], + "@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="], "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], @@ -2217,6 +2219,8 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/type-utils": "8.24.0", "@typescript-eslint/utils": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA=="], @@ -2365,13 +2369,13 @@ "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], - "autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="], + "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "axe-core": ["axe-core@4.10.2", "", {}, "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w=="], - "axios": ["axios@1.12.1", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ=="], + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -2451,7 +2455,7 @@ "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], - "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": "cli.js" }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], @@ -2861,7 +2865,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": "bin/cli.js" }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], @@ -3023,7 +3027,9 @@ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fast-xml-parser": ["fast-xml-parser@5.3.8", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-53jIF4N6u/pxvaL1eb/hEZts/cFLWZ92eCfLrNyCI0k38lettCG/Bs40W9pPwoPXyHQlKu2OUbQtiEIZK/J6Vw=="], + "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], + + "fast-xml-parser": ["fast-xml-parser@5.5.7", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.1.3", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg=="], "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="], @@ -3069,7 +3075,7 @@ "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], - "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "for-each": ["for-each@0.3.3", "", { "dependencies": { "is-callable": "^1.1.3" } }, "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw=="], @@ -3395,7 +3401,7 @@ "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -3879,8 +3885,6 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], - "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], @@ -4009,6 +4013,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-expression-matcher": ["path-expression-matcher@1.2.0", "", {}, "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -4055,7 +4061,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], - "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@8.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fovIPEV35c2JzVXdmP+sp2xirbBMt54J+upU8u6TSj410kUU5+axgEzvBBSAX8KCybze8CFCelzFAw/FfWg2TA=="], @@ -4299,7 +4305,7 @@ "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -4781,7 +4787,7 @@ "upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], - "update-browserslist-db": ["update-browserslist-db@1.1.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -4961,7 +4967,7 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yauzl": ["yauzl@3.2.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w=="], + "yauzl": ["yauzl@3.2.1", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A=="], "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], @@ -5227,6 +5233,78 @@ "@aws-sdk/client-kendra/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.18", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-ini": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw=="], + + "@aws-sdk/client-s3/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/client-s3/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/client-s3/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/client-s3/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.8", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A=="], + + "@aws-sdk/client-s3/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/client-s3/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/client-s3/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/client-s3/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/client-s3/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.4", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q=="], + + "@aws-sdk/client-s3/@smithy/config-resolver": ["@smithy/config-resolver@4.4.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg=="], + + "@aws-sdk/client-s3/@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], + + "@aws-sdk/client-s3/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA=="], + + "@aws-sdk/client-s3/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA=="], + + "@aws-sdk/client-s3/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw=="], + + "@aws-sdk/client-s3/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], + + "@aws-sdk/client-s3/@smithy/hash-node": ["@smithy/hash-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A=="], + + "@aws-sdk/client-s3/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g=="], + + "@aws-sdk/client-s3/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw=="], + + "@aws-sdk/client-s3/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], + + "@aws-sdk/client-s3/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.40", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/service-error-classification": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA=="], + + "@aws-sdk/client-s3/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + + "@aws-sdk/client-s3/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], + + "@aws-sdk/client-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], + + "@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], + + "@aws-sdk/client-s3/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@aws-sdk/client-s3/@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], + + "@aws-sdk/client-s3/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/client-s3/@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], + + "@aws-sdk/client-s3/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.39", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ=="], + + "@aws-sdk/client-s3/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.42", "", { "dependencies": { "@smithy/config-resolver": "^4.4.10", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A=="], + + "@aws-sdk/client-s3/@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA=="], + + "@aws-sdk/client-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], + + "@aws-sdk/client-s3/@smithy/util-retry": ["@smithy/util-retry@4.2.11", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw=="], + + "@aws-sdk/client-s3/@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], + "@aws-sdk/client-sso/@aws-sdk/core": ["@aws-sdk/core@3.623.0", "", { "dependencies": { "@smithy/core": "^2.3.2", "@smithy/node-config-provider": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/signature-v4": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8Toq3X6trX/67obSdh4K0MFQY4f132bEbr1i0YPDWk/O3KdBt12mLC/sW3aVRnlIs110XMuX9yrWWqJ8fDW10g=="], "@aws-sdk/client-sso/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.620.0", "", { "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg=="], @@ -5369,7 +5447,9 @@ "@aws-sdk/client-sso-oidc/@smithy/util-utf8": ["@smithy/util-utf8@3.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA=="], - "@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + "@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], + + "@aws-sdk/crc64-nvme/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.609.0", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q=="], @@ -5399,23 +5479,23 @@ "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@aws-sdk/credential-provider-login/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + "@aws-sdk/credential-provider-login/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-vI0QN96DFx3g9AunfOWF3CS4cMkqFiR/WM/FyP9QHr5rZ2dKPkYwP3tCgAOvGuu9CXI7dC1vU2FVUuZ+tfpNvQ=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-aS/81smalpe7XDnuQfOq4LIPuaV2PRKU2aMTrHcqO5BD4HwO5kESOHNcec2AYfBtLtIDqgF6RXisgBnfK/jt0w=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-login": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/credential-provider-env": "^3.972.20", "@aws-sdk/credential-provider-http": "^3.972.22", "@aws-sdk/credential-provider-login": "^3.972.22", "@aws-sdk/credential-provider-process": "^3.972.20", "@aws-sdk/credential-provider-sso": "^3.972.22", "@aws-sdk/credential-provider-web-identity": "^3.972.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-rpF8fBT0LllMDp78s62aL2A/8MaccjyJ0ORzqu+ZADeECLSrrCWIeeXsuRam+pxiAMkI1uIyDZJmgLGdadkPXw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-QRfk7GbA4/HDRjhP3QYR6QBr/QKreVoOzvvlRHnOuGgYJkeoPgPY3LAI1kK1ZMgZ4hH9KiGp757/ntol+INAig=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/token-providers": "3.1013.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-4vqlSaUbBj4aNPVKfB6yXuIQ2Z2mvLfIGba2OzzF6zUkN437/PGWsxBU2F8QPSFHti6seckvyCXidU3H+R8NvQ=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/wN1CYg2rVLhW8/jLxMWacQrkpaynnL+4j/Z+e6X1PfoE6NiC0BeOw3i0JmtZrKun85wNV5GmspvuWJihfeeUw=="], - "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], + "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="], - "@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + "@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.609.0", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q=="], @@ -5441,7 +5521,63 @@ "@aws-sdk/credential-providers/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@aws-sdk/middleware-websocket/@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA=="], + "@aws-sdk/middleware-bucket-endpoint/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/middleware-bucket-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], + + "@aws-sdk/middleware-bucket-endpoint/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@aws-sdk/middleware-bucket-endpoint/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/middleware-expect-continue/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/middleware-expect-continue/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@aws-sdk/middleware-expect-continue/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], + + "@aws-sdk/middleware-location-constraint/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/middleware-location-constraint/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], + + "@aws-sdk/middleware-ssec/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/middleware-ssec/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/middleware-websocket/@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A=="], "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.758.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-0RPCo8fYJcrenJ6bRtiUbFOSgQ1CX/GpvwtLU2Fam1tS9h2klKK8d74caeV6A1mIUvBU7bhyQ0wMGlwMtn3EYw=="], @@ -5455,7 +5591,15 @@ "@aws-sdk/s3-request-presigner/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], - "@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + "@aws-sdk/signature-v4-multi-region/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], "@aws-sdk/util-format-url/@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], @@ -5475,24 +5619,82 @@ "@azure/storage-common/@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-wrap-function/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/helper-wrap-function/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/helper-wrap-function/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-classes/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-computed-properties/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-destructuring/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + "@babel/plugin-transform-function-name/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + "@babel/plugin-transform-object-rest-spread/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-react-jsx/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.8", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg=="], @@ -5507,7 +5709,7 @@ "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/preset-modules/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], "@codesandbox/sandpack-client/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -5585,6 +5787,8 @@ "@jest/test-sequencer/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "@jest/transform/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + "@jest/transform/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -5617,14 +5821,16 @@ "@langchain/mistralai/uuid": ["uuid@10.0.0", "", { "bin": "dist/bin/uuid" }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "@librechat/client/rollup": ["rollup@4.37.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1004.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.18", "@aws-sdk/eventstream-handler-node": "^3.972.10", "@aws-sdk/middleware-eventstream": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/middleware-websocket": "^3.972.12", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw=="], + + "@librechat/backend/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], + + "@librechat/client/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], "@librechat/frontend/@react-spring/web": ["@react-spring/web@9.7.5", "", { "dependencies": { "@react-spring/animated": "~9.7.5", "@react-spring/core": "~9.7.5", "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ=="], "@librechat/frontend/@testing-library/jest-dom": ["@testing-library/jest-dom@5.17.0", "", { "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.5.6", "lodash": "^4.17.15", "redent": "^3.0.0" } }, "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg=="], - "@librechat/frontend/dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="], - "@librechat/frontend/framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], "@librechat/frontend/lucide-react": ["lucide-react@0.394.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw=="], @@ -5891,26 +6097,50 @@ "@smithy/credential-provider-imds/@smithy/url-parser": ["@smithy/url-parser@3.0.3", "", { "dependencies": { "@smithy/querystring-parser": "^3.0.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A=="], - "@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + "@smithy/hash-blob-browser/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@smithy/hash-stream-node/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@smithy/md5-js/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], "@smithy/property-provider/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + "@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], - "@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], + "@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="], - "@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + "@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], + + "@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], + + "@smithy/util-waiter/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], "@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], "@tanstack/match-sorter-utils/remove-accents": ["remove-accents@0.4.2", "", {}, "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="], + "@testing-library/dom/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@testing-library/dom/aria-query": ["aria-query@5.1.3", "", { "dependencies": { "deep-equal": "^2.0.5" } }, "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], "@testing-library/dom/pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "@types/babel__core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@types/babel__core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@types/babel__generator/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@types/babel__template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@types/babel__template/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@types/babel__traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@types/body-parser/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], "@types/connect/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], @@ -5951,8 +6181,6 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "@vitejs/plugin-react/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -5961,12 +6189,14 @@ "asn1.js/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], - "autoprefixer/fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "babel-plugin-root-import/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "babel-plugin-transform-import-meta/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "body-parser/qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], @@ -5979,7 +6209,7 @@ "browserify-sign/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], - "browserify-sign/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], "bun-types/@types/node": ["@types/node@20.11.16", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ=="], @@ -6003,6 +6233,8 @@ "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "cookie-parser/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], "core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], @@ -6027,12 +6259,16 @@ "data-urls/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "deep-equal/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "diffie-hellman/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -6137,6 +6373,10 @@ "is-bun-module/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "istanbul-lib-instrument/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "istanbul-lib-instrument/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "istanbul-lib-instrument/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "istanbul-lib-report/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], @@ -6159,6 +6399,8 @@ "jest-circus/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "jest-config/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + "jest-config/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "jest-config/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], @@ -6181,6 +6423,8 @@ "jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + "jest-message-util/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "jest-message-util/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -6201,6 +6445,12 @@ "jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + "jest-snapshot/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "jest-snapshot/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "jest-snapshot/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "jest-snapshot/@jest/expect-utils": ["@jest/expect-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0" } }, "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA=="], "jest-snapshot/expect": ["expect@30.2.0", "", { "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw=="], @@ -6235,8 +6485,6 @@ "jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "jwks-rsa/@types/express": ["@types/express@4.17.21", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ=="], "jwks-rsa/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], @@ -6295,8 +6543,12 @@ "mongodb-memory-server-core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "mongodb-memory-server-core/follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + "mongodb-memory-server-core/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "mongodb-memory-server-core/yauzl": ["yauzl@3.2.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w=="], + "mquery/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "multer/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], @@ -6307,6 +6559,8 @@ "node-stdlib-browser/pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="], + "node-stdlib-browser/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "nodemon/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "nodemon/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -6321,6 +6575,8 @@ "parse-entities/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], + "parse-json/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], @@ -6333,8 +6589,6 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "postcss/nanoid": ["nanoid@3.3.8", "", { "bin": "bin/nanoid.cjs" }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], - "postcss-attribute-case-insensitive/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "postcss-colormin/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], @@ -6367,10 +6621,6 @@ "postcss-normalize-unicode/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - "postcss-preset-env/autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], - - "postcss-preset-env/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "postcss-pseudo-class-any-link/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "postcss-reduce-initial/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], @@ -6391,6 +6641,8 @@ "read-cache/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": "bin/jsesc" }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], @@ -6429,6 +6681,10 @@ "router/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "send/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "sharp/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -6443,6 +6699,10 @@ "static-browser-server/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "stream-http/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -6473,6 +6733,8 @@ "tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "tailwindcss/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], + "tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" } }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], "tailwindcss/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], @@ -6485,6 +6747,8 @@ "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "unified/vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -6511,11 +6775,13 @@ "vfile-location/vfile": ["vfile@5.3.7", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g=="], - "vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "winston/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "winston-daily-rotate-file/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], - "workbox-build/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "workbox-build/@rollup/plugin-replace": ["@rollup/plugin-replace@2.4.2", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0" } }, "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg=="], @@ -6733,6 +6999,58 @@ "@aws-sdk/client-kendra/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/client-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-login": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], + + "@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], + + "@aws-sdk/client-s3/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@aws-sdk/client-s3/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/client-s3/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], + + "@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], + + "@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@aws-sdk/client-s3/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], + + "@aws-sdk/client-s3/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], + + "@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/client-s3/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], + "@aws-sdk/client-sso-oidc/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@4.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag=="], "@aws-sdk/client-sso-oidc/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], @@ -6821,6 +7139,46 @@ "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], + "@aws-sdk/middleware-bucket-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/middleware-bucket-endpoint/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-6mJ2zyyHPYSV6bAcaFpsdoXZJeQlR1QgBnZZ6juY/+dcYiuyWCdyLUbGzSZSE7GTfx6i+9+QWFeoIMlWKgU63A=="], "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], @@ -6843,20 +7201,216 @@ "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], + "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], + "@aws-sdk/util-format-url/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-create-class-features-plugin/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/helper-member-expression-to-functions/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/helper-module-imports/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/helper-replace-supers/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/helper-wrap-function/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-wrap-function/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-wrap-function/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-wrap-function/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-wrap-function/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/helper-wrap-function/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-classes/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-computed-properties/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-computed-properties/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-computed-properties/@babel/template/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + "@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-function-name/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], @@ -6917,6 +7471,24 @@ "@jest/reporters/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "@jest/transform/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@jest/transform/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@jest/transform/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@jest/transform/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@jest/transform/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@jest/transform/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@jest/transform/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@jest/transform/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@jest/transform/@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.927.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@aws-sdk/xml-builder": "3.921.0", "@smithy/core": "^3.17.2", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/signature-v4": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag=="], @@ -7035,41 +7607,91 @@ "@langchain/google-gauth/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], - "@librechat/client/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.37.0", "", { "os": "android", "cpu": "arm" }, "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], - "@librechat/client/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.37.0", "", { "os": "android", "cpu": "arm64" }, "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.18", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-ini": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw=="], - "@librechat/client/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.37.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA=="], - "@librechat/client/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.37.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g=="], - "@librechat/client/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.37.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], - "@librechat/client/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.37.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], - "@librechat/client/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], - "@librechat/client/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.8", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A=="], - "@librechat/client/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-format-url": "^3.972.7", "@smithy/eventstream-codec": "^4.2.11", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q=="], - "@librechat/client/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], - "@librechat/client/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1004.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA=="], - "@librechat/client/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], - "@librechat/client/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.37.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], - "@librechat/client/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], - "@librechat/client/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.4", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q=="], - "@librechat/client/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.37.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/config-resolver": ["@smithy/config-resolver@4.4.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg=="], - "@librechat/client/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.37.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], - "@librechat/client/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/hash-node": ["@smithy/hash-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.40", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/service-error-classification": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.39", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.42", "", { "dependencies": { "@smithy/config-resolver": "^4.4.10", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-retry": ["@smithy/util-retry@4.2.11", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], + + "@librechat/backend/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], + + "@librechat/backend/@smithy/node-http-handler/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@librechat/backend/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@librechat/backend/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], "@librechat/frontend/@react-spring/web/@react-spring/animated": ["@react-spring/animated@9.7.5", "", { "dependencies": { "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg=="], @@ -7239,6 +7861,8 @@ "@types/winston/winston/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], + "@types/winston/winston/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "@types/winston/winston/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], "@types/xml-encryption/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -7247,36 +7871,22 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@vitejs/plugin-react/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@vitejs/plugin-react/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "@vitejs/plugin-react/@babel/core/@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - - "@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - - "@vitejs/plugin-react/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@vitejs/plugin-react/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@vitejs/plugin-react/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "babel-plugin-transform-import-meta/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "babel-plugin-transform-import-meta/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "babel-plugin-transform-import-meta/@babel/template/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "body-parser/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "browserify-sign/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "caniuse-api/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "caniuse-api/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "caniuse-api/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -7293,6 +7903,8 @@ "core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "core-js-compat/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], @@ -7307,6 +7919,8 @@ "eslint/@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "expect/jest-message-util/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "expect/jest-message-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], "expect/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -7357,6 +7971,26 @@ "hastscript/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], + "istanbul-lib-instrument/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "istanbul-lib-instrument/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "istanbul-lib-instrument/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "istanbul-lib-instrument/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "istanbul-lib-instrument/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "istanbul-lib-instrument/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "istanbul-lib-instrument/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "istanbul-lib-instrument/@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "istanbul-lib-instrument/@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "istanbul-lib-instrument/@babel/parser/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "istanbul-lib-report/make-dir/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "istanbul-lib-report/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -7377,6 +8011,24 @@ "jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-config/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "jest-config/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "jest-config/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "jest-config/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "jest-config/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "jest-config/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "jest-config/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "jest-config/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "jest-config/@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "jest-config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "jest-config/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -7411,6 +8063,26 @@ "jest-runtime/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "jest-snapshot/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "jest-snapshot/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "jest-snapshot/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "jest-snapshot/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "jest-snapshot/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "jest-snapshot/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "jest-snapshot/@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "jest-snapshot/@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "jest-snapshot/@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "jest-snapshot/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "jest-snapshot/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-snapshot/synckit/@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], @@ -7429,10 +8101,6 @@ "jsonwebtoken/jws/jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], - "jszip/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "jwks-rsa/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -7457,34 +8125,38 @@ "postcss-colormin/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-colormin/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "postcss-colormin/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "postcss-convert-values/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-convert-values/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "postcss-convert-values/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "postcss-merge-rules/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-merge-rules/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "postcss-merge-rules/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "postcss-minify-params/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-minify-params/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "postcss-minify-params/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "postcss-normalize-unicode/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-normalize-unicode/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "postcss-normalize-unicode/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - "postcss-preset-env/autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - - "postcss-preset-env/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - - "postcss-preset-env/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - - "postcss-preset-env/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "postcss-reduce-initial/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-reduce-initial/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "postcss-reduce-initial/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], @@ -7519,6 +8191,8 @@ "stylehacks/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "stylehacks/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "stylehacks/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -7531,6 +8205,8 @@ "svgo/css-select/domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + "tailwindcss/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": "bin/nanoid.cjs" }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + "tailwindcss/postcss-load-config/lilconfig": ["lilconfig@3.0.0", "", {}, "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g=="], "unist-util-remove-position/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], @@ -7545,23 +8221,7 @@ "winston-daily-rotate-file/winston-transport/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], - "workbox-build/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "workbox-build/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - - "workbox-build/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "workbox-build/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "workbox-build/@babel/core/@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - - "workbox-build/@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - - "workbox-build/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "workbox-build/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "workbox-build/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "winston-daily-rotate-file/winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "workbox-build/@rollup/plugin-replace/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], @@ -7683,6 +8343,20 @@ "@aws-sdk/client-kendra/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1004.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + + "@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + "@aws-sdk/client-sso-oidc/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], "@aws-sdk/client-sso-oidc/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ=="], @@ -7743,6 +8417,30 @@ "@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], + + "@aws-sdk/middleware-flexible-checksums/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.758.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg=="], "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.723.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w=="], @@ -7799,6 +8497,32 @@ "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], + "@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-wrap-function/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], @@ -7811,12 +8535,54 @@ "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + "@babel/plugin-transform-function-name/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], @@ -7971,6 +8737,68 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": "dist/esm/bin.mjs" }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-login": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/eventstream-handler-node/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], + "@librechat/frontend/@react-spring/web/@react-spring/shared/@react-spring/rafz": ["@react-spring/rafz@9.7.5", "", {}, "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw=="], "@librechat/frontend/@testing-library/jest-dom/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -8001,16 +8829,6 @@ "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], - "@vitejs/plugin-react/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - "body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -8041,8 +8859,12 @@ "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "istanbul-lib-instrument/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "jest-changed-files/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "jest-config/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "jest-config/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "jest-config/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], @@ -8071,16 +8893,6 @@ "svgo/css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], - "workbox-build/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "workbox-build/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - - "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "workbox-build/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "workbox-build/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - "workbox-build/@rollup/plugin-replace/@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], "workbox-build/@rollup/plugin-replace/@rollup/pluginutils/estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], @@ -8129,6 +8941,14 @@ "@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@3.0.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ=="], + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], @@ -8175,8 +8995,16 @@ "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "@google/genai/google-auth-library/gaxios/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -8539,20 +9367,26 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@aws-sdk/util-format-url/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + + "@librechat/backend/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + "@librechat/frontend/@testing-library/jest-dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "@node-saml/passport-saml/@types/express/@types/express-serve-static-core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "expect/jest-message-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], "expect/jest-message-util/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -8567,20 +9401,14 @@ "svgo/css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], - "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - - "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - - "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "workbox-build/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "workbox-build/source-map/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], diff --git a/client/jest.config.cjs b/client/jest.config.cjs index f97adb39ce..4d9087bff7 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -1,4 +1,4 @@ -/** v0.8.4-rc1 */ +/** v0.8.4 */ module.exports = { roots: ['/src'], testEnvironment: 'jsdom', diff --git a/client/package.json b/client/package.json index f42834c1c2..c23c44804c 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/frontend", - "version": "v0.8.4-rc1", + "version": "v0.8.4", "description": "", "type": "module", "scripts": { diff --git a/e2e/jestSetup.js b/e2e/jestSetup.js index f6d5bf4c66..93d72d7672 100644 --- a/e2e/jestSetup.js +++ b/e2e/jestSetup.js @@ -1,3 +1,3 @@ -// v0.8.4-rc1 +// v0.8.4 // See .env.test.example for an example of the '.env.test' file. require('dotenv').config({ path: './e2e/.env.test' }); diff --git a/helm/librechat/Chart.yaml b/helm/librechat/Chart.yaml index d39ec8811c..80c64771ad 100755 --- a/helm/librechat/Chart.yaml +++ b/helm/librechat/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.0.1 +version: 2.0.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,7 +23,7 @@ version: 2.0.1 # It is recommended to use it with quotes. # renovate: image=registry.librechat.ai/danny-avila/librechat -appVersion: "v0.8.4-rc1" +appVersion: "v0.8.4" home: https://www.librechat.ai diff --git a/package-lock.json b/package-lock.json index 5d8264f602..09b994d719 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "LibreChat", - "version": "v0.8.4-rc1", + "version": "v0.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "LibreChat", - "version": "v0.8.4-rc1", + "version": "v0.8.4", "license": "ISC", "workspaces": [ "api", @@ -46,7 +46,7 @@ }, "api": { "name": "@librechat/backend", - "version": "v0.8.4-rc1", + "version": "v0.8.4", "license": "ISC", "dependencies": { "@anthropic-ai/vertex-sdk": "^0.14.3", @@ -430,7 +430,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "v0.8.4-rc1", + "version": "v0.8.4", "license": "ISC", "dependencies": { "@ariakit/react": "^0.4.15", @@ -43808,7 +43808,7 @@ }, "packages/api": { "name": "@librechat/api", - "version": "1.7.26", + "version": "1.7.27", "license": "ISC", "devDependencies": { "@babel/preset-env": "^7.21.5", @@ -43928,7 +43928,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.4.55", + "version": "0.4.56", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -45752,7 +45752,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.400", + "version": "0.8.401", "license": "ISC", "dependencies": { "axios": "^1.13.5", @@ -45810,7 +45810,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.39", + "version": "0.0.40", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", diff --git a/package.json b/package.json index de6a580a1a..25e6da4ab2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LibreChat", - "version": "v0.8.4-rc1", + "version": "v0.8.4", "description": "", "packageManager": "npm@11.10.0", "workspaces": [ diff --git a/packages/api/package.json b/packages/api/package.json index 3a3b3caef6..9ca7f9f865 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/api", - "version": "1.7.26", + "version": "1.7.27", "type": "commonjs", "description": "MCP services for LibreChat", "main": "dist/index.js", diff --git a/packages/client/package.json b/packages/client/package.json index 908f9f98f7..801d3e389d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.4.55", + "version": "0.4.56", "description": "React components for LibreChat", "repository": { "type": "git", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index 09e13b31a7..3f6925c479 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.8.400", + "version": "0.8.401", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 0c8c591488..043e1952f3 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1744,7 +1744,7 @@ export enum TTSProviders { /** Enum for app-wide constants */ export enum Constants { /** Key for the app's version. */ - VERSION = 'v0.8.4-rc1', + VERSION = 'v0.8.4', /** Key for the Custom Config's version (librechat.yaml). */ CONFIG_VERSION = '1.3.6', /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */ diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index 87acd16f1e..0376804ad4 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/data-schemas", - "version": "0.0.39", + "version": "0.0.40", "description": "Mongoose schemas and models for LibreChat", "type": "module", "main": "dist/index.cjs", From 290984c51469b42c7323ab293560f02be5881752 Mon Sep 17 00:00:00 2001 From: crossagent <150273112@qq.com> Date: Sun, 22 Mar 2026 00:46:23 +0800 Subject: [PATCH 092/111] =?UTF-8?q?=F0=9F=94=91=20fix:=20Type-Safe=20User?= =?UTF-8?q?=20Context=20Forwarding=20for=20Non-OAuth=20Tool=20Discovery=20?= =?UTF-8?q?(#12348)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mcp): pass missing customUserVars and user during unauthenticated tool discovery * fix(mcp): type-safe user context forwarding for non-OAuth tool discovery Extract UserConnectionContext from OAuthConnectionOptions to properly model the non-OAuth case where user/customUserVars/requestBody need placeholder resolution without requiring OAuth-specific fields. - Remove prohibited `as unknown as` double-cast - Forward requestBody and connectionTimeout (previously omitted) - Add unit tests for argument forwarding at Manager and Factory layers - Add integration test exercising real processMCPEnv substitution --------- Co-authored-by: Danny Avila --- packages/api/src/mcp/MCPConnectionFactory.ts | 45 ++++++----- packages/api/src/mcp/MCPManager.ts | 7 +- .../__tests__/MCPConnectionFactory.test.ts | 33 +++++++++ .../api/src/mcp/__tests__/MCPManager.test.ts | 40 ++++++++++ .../customUserVars.integration.test.ts | 74 +++++++++++++++++++ packages/api/src/mcp/types/index.ts | 12 ++- 6 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 packages/api/src/mcp/__tests__/customUserVars.integration.test.ts diff --git a/packages/api/src/mcp/MCPConnectionFactory.ts b/packages/api/src/mcp/MCPConnectionFactory.ts index 2c16da0760..337662c812 100644 --- a/packages/api/src/mcp/MCPConnectionFactory.ts +++ b/packages/api/src/mcp/MCPConnectionFactory.ts @@ -58,9 +58,13 @@ export class MCPConnectionFactory { */ static async discoverTools( basic: t.BasicConnectionOptions, - oauth?: Omit, + options?: Omit | t.UserConnectionContext, ): Promise { - const factory = new this(basic, oauth ? { ...oauth, returnOnOAuth: true } : undefined); + if (options != null && 'useOAuth' in options) { + const factory = new this(basic, { ...options, returnOnOAuth: true }); + return factory.discoverToolsInternal(); + } + const factory = new this(basic, options); return factory.discoverToolsInternal(); } @@ -187,31 +191,36 @@ export class MCPConnectionFactory { return null; } - protected constructor(basic: t.BasicConnectionOptions, oauth?: t.OAuthConnectionOptions) { + protected constructor( + basic: t.BasicConnectionOptions, + options?: t.OAuthConnectionOptions | t.UserConnectionContext, + ) { this.serverConfig = processMCPEnv({ - user: oauth?.user, - body: oauth?.requestBody, + user: options?.user, + body: options?.requestBody, dbSourced: basic.dbSourced, options: basic.serverConfig, - customUserVars: oauth?.customUserVars, + customUserVars: options?.customUserVars, }); this.serverName = basic.serverName; - this.useOAuth = !!oauth?.useOAuth; this.useSSRFProtection = basic.useSSRFProtection === true; this.allowedDomains = basic.allowedDomains; - this.connectionTimeout = oauth?.connectionTimeout; - this.logPrefix = oauth?.user - ? `[MCP][${basic.serverName}][${oauth.user.id}]` + this.connectionTimeout = options?.connectionTimeout; + this.logPrefix = options?.user + ? `[MCP][${basic.serverName}][${options.user.id}]` : `[MCP][${basic.serverName}]`; - if (oauth?.useOAuth) { - this.userId = oauth.user?.id; - this.flowManager = oauth.flowManager; - this.tokenMethods = oauth.tokenMethods; - this.signal = oauth.signal; - this.oauthStart = oauth.oauthStart; - this.oauthEnd = oauth.oauthEnd; - this.returnOnOAuth = oauth.returnOnOAuth; + if (options != null && 'useOAuth' in options) { + this.useOAuth = true; + this.userId = options.user?.id; + this.flowManager = options.flowManager; + this.tokenMethods = options.tokenMethods; + this.signal = options.signal; + this.oauthStart = options.oauthStart; + this.oauthEnd = options.oauthEnd; + this.returnOnOAuth = options.returnOnOAuth; + } else { + this.useOAuth = false; } } diff --git a/packages/api/src/mcp/MCPManager.ts b/packages/api/src/mcp/MCPManager.ts index afb6c68796..935307fa49 100644 --- a/packages/api/src/mcp/MCPManager.ts +++ b/packages/api/src/mcp/MCPManager.ts @@ -113,7 +113,12 @@ export class MCPManager extends UserConnectionManager { }; if (!useOAuth) { - const result = await MCPConnectionFactory.discoverTools(basic); + const result = await MCPConnectionFactory.discoverTools(basic, { + user: args.user, + customUserVars: args.customUserVars, + requestBody: args.requestBody, + connectionTimeout: args.connectionTimeout, + }); return { tools: result.tools, oauthRequired: result.oauthRequired, diff --git a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts index 23bfa89d56..75d7b4321d 100644 --- a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts +++ b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts @@ -764,6 +764,39 @@ describe('MCPConnectionFactory', () => { expect(result.connection).toBe(mockConnectionInstance); }); + it('should forward user context to processMCPEnv for non-OAuth discovery', async () => { + const serverConfig: t.MCPOptions = { + type: 'streamable-http', + url: 'https://my-mcp.server.com?key={{MY_CUSTOM_KEY}}', + } as t.MCPOptions; + + const basicOptions = { + serverName: 'test-server', + serverConfig, + }; + + const userContext = { + user: mockUser, + customUserVars: { MY_CUSTOM_KEY: 'c527bd0abc123' }, + connectionTimeout: 10000, + }; + + mockConnectionInstance.connect.mockResolvedValue(undefined); + mockConnectionInstance.isConnected.mockResolvedValue(true); + mockConnectionInstance.fetchTools = jest.fn().mockResolvedValue(mockTools); + + const result = await MCPConnectionFactory.discoverTools(basicOptions, userContext); + + expect(result.tools).toEqual(mockTools); + expect(mockProcessMCPEnv).toHaveBeenCalledWith( + expect.objectContaining({ + user: mockUser, + options: serverConfig, + customUserVars: { MY_CUSTOM_KEY: 'c527bd0abc123' }, + }), + ); + }); + it('should detect OAuth required without generating URL in discovery mode', async () => { const basicOptions = { serverName: 'test-server', diff --git a/packages/api/src/mcp/__tests__/MCPManager.test.ts b/packages/api/src/mcp/__tests__/MCPManager.test.ts index dd1ead0dd9..ba5b0b3b8e 100644 --- a/packages/api/src/mcp/__tests__/MCPManager.test.ts +++ b/packages/api/src/mcp/__tests__/MCPManager.test.ts @@ -847,6 +847,46 @@ describe('MCPManager', () => { expect(MCPConnectionFactory.discoverTools).toHaveBeenCalled(); }); + it('should forward user, customUserVars, requestBody, and connectionTimeout to discoverTools in the non-OAuth path', async () => { + const mockUser = { id: 'user123', email: 'test@example.com' } as unknown as IUser; + const customUserVars = { MY_CUSTOM_KEY: 'c527bd0abc123' }; + + mockAppConnections({ + get: jest.fn().mockResolvedValue(null), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue({ + type: 'streamable-http', + url: 'https://my-mcp.server.com?key={{MY_CUSTOM_KEY}}', + }); + + (MCPConnectionFactory.discoverTools as jest.Mock).mockResolvedValue({ + tools: mockTools, + connection: null, + oauthRequired: false, + oauthUrl: null, + }); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + await manager.discoverServerTools({ + serverName, + user: mockUser, + customUserVars, + requestBody: { conversationId: 'conv-123' } as t.ToolDiscoveryOptions['requestBody'], + connectionTimeout: 10000, + }); + + expect(MCPConnectionFactory.discoverTools).toHaveBeenCalledWith( + expect.objectContaining({ serverName }), + expect.objectContaining({ + user: mockUser, + customUserVars, + requestBody: { conversationId: 'conv-123' }, + connectionTimeout: 10000, + }), + ); + }); + it('should return null tools when server config not found', async () => { mockAppConnections({ get: jest.fn().mockResolvedValue(null), diff --git a/packages/api/src/mcp/__tests__/customUserVars.integration.test.ts b/packages/api/src/mcp/__tests__/customUserVars.integration.test.ts new file mode 100644 index 0000000000..35e087be04 --- /dev/null +++ b/packages/api/src/mcp/__tests__/customUserVars.integration.test.ts @@ -0,0 +1,74 @@ +/** + * Integration test exercising real processMCPEnv for the non-OAuth + * customUserVars scenario: a streamable-http server whose URL contains + * a {{PLACEHOLDER}} that must be resolved from per-user custom variables. + * + * This is the exact bug scenario from PR #12348 — without the fix, + * the literal string `{{MY_CUSTOM_KEY}}` would be sent to the MCP + * server endpoint instead of the substituted value. + */ +import type { IUser } from '@librechat/data-schemas'; +import type * as t from '~/mcp/types'; +import { processMCPEnv } from '~/utils/env'; + +describe('processMCPEnv — customUserVars placeholder resolution', () => { + const mockUser = { id: 'user-abc', email: 'test@example.com' } as IUser; + + it('should resolve {{CUSTOM_VAR}} in a streamable-http URL', () => { + const serverConfig: t.MCPOptions = { + type: 'streamable-http', + url: 'https://my-mcp.server.com/server?key={{MY_CUSTOM_KEY}}', + } as t.MCPOptions; + + const result = processMCPEnv({ + options: serverConfig, + user: mockUser, + customUserVars: { MY_CUSTOM_KEY: 'c527bd0abc123' }, + }); + + expect((result as t.StreamableHTTPOptions).url).toBe( + 'https://my-mcp.server.com/server?key=c527bd0abc123', + ); + }); + + it('should resolve multiple placeholders in URL and headers simultaneously', () => { + const serverConfig: t.MCPOptions = { + type: 'streamable-http', + url: 'https://my-mcp.server.com/server?key={{API_KEY}}&project={{PROJECT_ID}}', + headers: { + Authorization: 'Bearer {{AUTH_TOKEN}}', + 'X-Project': '{{PROJECT_ID}}', + }, + } as t.MCPOptions; + + const result = processMCPEnv({ + options: serverConfig, + user: mockUser, + customUserVars: { + API_KEY: 'key-123', + PROJECT_ID: 'proj-456', + AUTH_TOKEN: 'tok-789', + }, + }); + + const typed = result as t.StreamableHTTPOptions; + expect(typed.url).toBe('https://my-mcp.server.com/server?key=key-123&project=proj-456'); + expect(typed.headers).toEqual({ + Authorization: 'Bearer tok-789', + 'X-Project': 'proj-456', + }); + }); + + it('should leave unmatched placeholders as literal strings when customUserVars is undefined', () => { + const serverConfig: t.MCPOptions = { + type: 'streamable-http', + url: 'https://my-mcp.server.com/server?key={{MY_CUSTOM_KEY}}', + } as t.MCPOptions; + + const result = processMCPEnv({ + options: serverConfig, + }); + + expect((result as t.StreamableHTTPOptions).url).toContain('{{MY_CUSTOM_KEY}}'); + }); +}); diff --git a/packages/api/src/mcp/types/index.ts b/packages/api/src/mcp/types/index.ts index 0af10c7399..9d43aa543d 100644 --- a/packages/api/src/mcp/types/index.ts +++ b/packages/api/src/mcp/types/index.ts @@ -174,18 +174,22 @@ export interface BasicConnectionOptions { dbSourced?: boolean; } -export interface OAuthConnectionOptions { +/** User context for placeholder resolution in MCP connections (non-OAuth and OAuth alike) */ +export interface UserConnectionContext { user?: IUser; - useOAuth: true; - requestBody?: RequestBody; customUserVars?: Record; + requestBody?: RequestBody; + connectionTimeout?: number; +} + +export interface OAuthConnectionOptions extends UserConnectionContext { + useOAuth: true; flowManager: FlowStateManager; tokenMethods?: TokenMethods; signal?: AbortSignal; oauthStart?: (authURL: string) => Promise; oauthEnd?: () => Promise; returnOnOAuth?: boolean; - connectionTimeout?: number; } export interface ToolDiscoveryOptions { From 38521381f4b0435b1ca3f1fa67fbb433c872f92c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 13 Feb 2026 02:14:34 -0500 Subject: [PATCH 093/111] =?UTF-8?q?=F0=9F=90=98=20feat:=20FerretDB=20Compa?= =?UTF-8?q?tibility=20(#11769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: replace unsupported MongoDB aggregation operators for FerretDB compatibility Replace $lookup, $unwind, $sample, $replaceRoot, and $addFields aggregation stages which are unsupported on FerretDB v2.x (postgres-documentdb backend). - Prompt.js: Replace $lookup/$unwind/$project pipelines with find().select().lean() + attachProductionPrompts() batch helper. Replace $group/$replaceRoot/$sample in getRandomPromptGroups with distinct() + Fisher-Yates shuffle. - Agent/Prompt migration scripts: Replace $lookup anti-join pattern with distinct() + $nin two-step queries for finding un-migrated resources. All replacement patterns verified against FerretDB v2.7.0. * fix: use $pullAll for simple array removals, fix memberIds type mismatches Replace $pull with $pullAll for exact-value scalar array removals. Both operators work on MongoDB and FerretDB, but $pullAll is more explicit for exact matching (no condition expressions). Fix critical type mismatch bugs where ObjectId values were used against String[] memberIds arrays in Group queries: - config/delete-user.js: use string uid instead of ObjectId user._id - e2e/setup/cleanupUser.ts: convert userId.toString() before query Harden PermissionService.bulkUpdateResourcePermissions abort handling to prevent crash when abortTransaction is called after commitTransaction. All changes verified against FerretDB v2.7.0 and MongoDB Memory Server. * fix: harden transaction support probe for FerretDB compatibility Commit the transaction before aborting in supportsTransactions probe, and wrap abortTransaction in try-catch to prevent crashes when abort is called after a successful commit (observed behavior on FerretDB). * feat: add FerretDB compatibility test suite, retry utilities, and CI config Add comprehensive FerretDB integration test suite covering: - $pullAll scalar array operations - $pull with subdocument conditions - $lookup replacement (find + manual join) - $sample replacement (distinct + Fisher-Yates) - $bit and $bitsAllSet operations - Migration anti-join pattern - Multi-tenancy (useDb, scaling, write amplification) - Sharding proof-of-concept - Production operations (backup/restore, schema migration, deadlock retry) Add production retryWithBackoff utility for deadlock recovery during concurrent index creation on FerretDB/DocumentDB backends. Add UserController.spec.js tests for deleteUserController (runs in CI). Configure jest and eslint to isolate FerretDB tests from CI pipelines: - packages/data-schemas/jest.config.mjs: ignore misc/ directory - eslint.config.mjs: ignore packages/data-schemas/misc/ Include Docker Compose config for local FerretDB v2.7 + postgres-documentdb, dedicated jest/tsconfig for the test files, and multi-tenancy findings doc. * style: brace formatting in aclEntry.ts modifyPermissionBits * refactor: reorganize retry utilities and update imports - Moved retryWithBackoff utility to a new file `retry.ts` for better structure. - Updated imports in `orgOperations.ferretdb.spec.ts` to reflect the new location of retry utilities. - Removed old import statement for retryWithBackoff from index.ts to streamline exports. * test: add $pullAll coverage for ConversationTag and PermissionService Add integration tests for deleteConversationTag verifying $pullAll removes tags from conversations correctly, and for syncUserEntraGroupMemberships verifying $pullAll removes user from non-matching Entra groups while preserving local group membership. --------- --- api/models/Agent.js | 9 +- api/models/ConversationTag.js | 2 +- api/models/ConversationTag.spec.js | 114 +++ api/models/Project.js | 8 +- api/models/Prompt.js | 221 ++---- api/server/controllers/UserController.js | 6 +- api/server/controllers/UserController.spec.js | 208 ++++++ api/server/controllers/agents/v1.spec.js | 1 - api/server/services/PermissionService.js | 12 +- api/server/services/PermissionService.spec.js | 136 ++++ config/delete-user.js | 2 +- config/migrate-agent-permissions.js | 55 +- config/migrate-prompt-permissions.js | 55 +- e2e/setup/cleanupUser.ts | 3 +- eslint.config.mjs | 1 + packages/api/src/agents/migration.ts | 56 +- packages/api/src/prompts/migration.ts | 56 +- packages/data-schemas/jest.config.mjs | 1 + .../misc/ferretdb/aclBitops.ferretdb.spec.ts | 468 ++++++++++++ .../misc/ferretdb/docker-compose.ferretdb.yml | 21 + .../ferretdb/ferretdb-multitenancy-plan.md | 204 ++++++ .../misc/ferretdb/jest.ferretdb.config.mjs | 18 + .../migrationAntiJoin.ferretdb.spec.ts | 362 ++++++++++ .../ferretdb/multiTenancy.ferretdb.spec.ts | 649 +++++++++++++++++ .../ferretdb/orgOperations.ferretdb.spec.ts | 675 ++++++++++++++++++ .../ferretdb/promptLookup.ferretdb.spec.ts | 353 +++++++++ .../misc/ferretdb/pullAll.ferretdb.spec.ts | 297 ++++++++ .../ferretdb/pullSubdocument.ferretdb.spec.ts | 199 ++++++ .../ferretdb/randomPrompts.ferretdb.spec.ts | 210 ++++++ .../misc/ferretdb/sharding.ferretdb.spec.ts | 522 ++++++++++++++ .../data-schemas/misc/ferretdb/tsconfig.json | 14 + packages/data-schemas/src/methods/aclEntry.ts | 4 +- .../data-schemas/src/methods/userGroup.ts | 2 +- packages/data-schemas/src/utils/retry.ts | 122 ++++ .../data-schemas/src/utils/transactions.ts | 8 +- 35 files changed, 4727 insertions(+), 347 deletions(-) create mode 100644 api/models/ConversationTag.spec.js create mode 100644 api/server/controllers/UserController.spec.js create mode 100644 packages/data-schemas/misc/ferretdb/aclBitops.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/docker-compose.ferretdb.yml create mode 100644 packages/data-schemas/misc/ferretdb/ferretdb-multitenancy-plan.md create mode 100644 packages/data-schemas/misc/ferretdb/jest.ferretdb.config.mjs create mode 100644 packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/multiTenancy.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/orgOperations.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/pullSubdocument.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/randomPrompts.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/sharding.ferretdb.spec.ts create mode 100644 packages/data-schemas/misc/ferretdb/tsconfig.json create mode 100644 packages/data-schemas/src/utils/retry.ts diff --git a/api/models/Agent.js b/api/models/Agent.js index 53098888d6..7c35260cd5 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -549,16 +549,15 @@ const removeAgentResourceFiles = async ({ agent_id, files }) => { return acc; }, {}); - // Step 1: Atomically remove file IDs using $pull - const pullOps = {}; + const pullAllOps = {}; const resourcesToCheck = new Set(); for (const [resource, fileIds] of Object.entries(filesByResource)) { const fileIdsPath = `tool_resources.${resource}.file_ids`; - pullOps[fileIdsPath] = { $in: fileIds }; + pullAllOps[fileIdsPath] = fileIds; resourcesToCheck.add(resource); } - const updatePullData = { $pull: pullOps }; + const updatePullData = { $pullAll: pullAllOps }; const agentAfterPull = await Agent.findOneAndUpdate(searchParameter, updatePullData, { new: true, }).lean(); @@ -818,7 +817,7 @@ const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds for (const projectId of removeProjectIds) { await removeAgentIdsFromProject(projectId, [agentId]); } - updateOps.$pull = { projectIds: { $in: removeProjectIds } }; + updateOps.$pullAll = { projectIds: removeProjectIds }; } if (projectIds && projectIds.length > 0) { diff --git a/api/models/ConversationTag.js b/api/models/ConversationTag.js index 47a6c2bbf5..99d0608a66 100644 --- a/api/models/ConversationTag.js +++ b/api/models/ConversationTag.js @@ -165,7 +165,7 @@ const deleteConversationTag = async (user, tag) => { return null; } - await Conversation.updateMany({ user, tags: tag }, { $pull: { tags: tag } }); + await Conversation.updateMany({ user, tags: tag }, { $pullAll: { tags: [tag] } }); await ConversationTag.updateMany( { user, position: { $gt: deletedTag.position } }, diff --git a/api/models/ConversationTag.spec.js b/api/models/ConversationTag.spec.js new file mode 100644 index 0000000000..bc7da919e1 --- /dev/null +++ b/api/models/ConversationTag.spec.js @@ -0,0 +1,114 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { ConversationTag, Conversation } = require('~/db/models'); +const { deleteConversationTag } = require('./ConversationTag'); + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +afterEach(async () => { + await ConversationTag.deleteMany({}); + await Conversation.deleteMany({}); +}); + +describe('ConversationTag model - $pullAll operations', () => { + const userId = new mongoose.Types.ObjectId().toString(); + + describe('deleteConversationTag', () => { + it('should remove the tag from all conversations that have it', async () => { + await ConversationTag.create({ tag: 'work', user: userId, position: 1 }); + + await Conversation.create([ + { conversationId: 'conv1', user: userId, endpoint: 'openAI', tags: ['work', 'important'] }, + { conversationId: 'conv2', user: userId, endpoint: 'openAI', tags: ['work'] }, + { conversationId: 'conv3', user: userId, endpoint: 'openAI', tags: ['personal'] }, + ]); + + await deleteConversationTag(userId, 'work'); + + const convos = await Conversation.find({ user: userId }).sort({ conversationId: 1 }).lean(); + expect(convos[0].tags).toEqual(['important']); + expect(convos[1].tags).toEqual([]); + expect(convos[2].tags).toEqual(['personal']); + }); + + it('should delete the tag document itself', async () => { + await ConversationTag.create({ tag: 'temp', user: userId, position: 1 }); + + const result = await deleteConversationTag(userId, 'temp'); + + expect(result).toBeDefined(); + expect(result.tag).toBe('temp'); + + const remaining = await ConversationTag.find({ user: userId }).lean(); + expect(remaining).toHaveLength(0); + }); + + it('should return null when the tag does not exist', async () => { + const result = await deleteConversationTag(userId, 'nonexistent'); + expect(result).toBeNull(); + }); + + it('should adjust positions of tags after the deleted one', async () => { + await ConversationTag.create([ + { tag: 'first', user: userId, position: 1 }, + { tag: 'second', user: userId, position: 2 }, + { tag: 'third', user: userId, position: 3 }, + ]); + + await deleteConversationTag(userId, 'first'); + + const tags = await ConversationTag.find({ user: userId }).sort({ position: 1 }).lean(); + expect(tags).toHaveLength(2); + expect(tags[0].tag).toBe('second'); + expect(tags[0].position).toBe(1); + expect(tags[1].tag).toBe('third'); + expect(tags[1].position).toBe(2); + }); + + it('should not affect conversations of other users', async () => { + const otherUser = new mongoose.Types.ObjectId().toString(); + + await ConversationTag.create({ tag: 'shared-name', user: userId, position: 1 }); + await ConversationTag.create({ tag: 'shared-name', user: otherUser, position: 1 }); + + await Conversation.create([ + { conversationId: 'mine', user: userId, endpoint: 'openAI', tags: ['shared-name'] }, + { conversationId: 'theirs', user: otherUser, endpoint: 'openAI', tags: ['shared-name'] }, + ]); + + await deleteConversationTag(userId, 'shared-name'); + + const myConvo = await Conversation.findOne({ conversationId: 'mine' }).lean(); + const theirConvo = await Conversation.findOne({ conversationId: 'theirs' }).lean(); + + expect(myConvo.tags).toEqual([]); + expect(theirConvo.tags).toEqual(['shared-name']); + }); + + it('should handle duplicate tags in conversations correctly', async () => { + await ConversationTag.create({ tag: 'dup', user: userId, position: 1 }); + + const conv = await Conversation.create({ + conversationId: 'conv-dup', + user: userId, + endpoint: 'openAI', + tags: ['dup', 'other', 'dup'], + }); + + await deleteConversationTag(userId, 'dup'); + + const updated = await Conversation.findById(conv._id).lean(); + expect(updated.tags).toEqual(['other']); + }); + }); +}); diff --git a/api/models/Project.js b/api/models/Project.js index 8fd1e556f9..dc92348b54 100644 --- a/api/models/Project.js +++ b/api/models/Project.js @@ -64,7 +64,7 @@ const addGroupIdsToProject = async function (projectId, promptGroupIds) { const removeGroupIdsFromProject = async function (projectId, promptGroupIds) { return await Project.findByIdAndUpdate( projectId, - { $pull: { promptGroupIds: { $in: promptGroupIds } } }, + { $pullAll: { promptGroupIds: promptGroupIds } }, { new: true }, ); }; @@ -76,7 +76,7 @@ const removeGroupIdsFromProject = async function (projectId, promptGroupIds) { * @returns {Promise} */ const removeGroupFromAllProjects = async (promptGroupId) => { - await Project.updateMany({}, { $pull: { promptGroupIds: promptGroupId } }); + await Project.updateMany({}, { $pullAll: { promptGroupIds: [promptGroupId] } }); }; /** @@ -104,7 +104,7 @@ const addAgentIdsToProject = async function (projectId, agentIds) { const removeAgentIdsFromProject = async function (projectId, agentIds) { return await Project.findByIdAndUpdate( projectId, - { $pull: { agentIds: { $in: agentIds } } }, + { $pullAll: { agentIds: agentIds } }, { new: true }, ); }; @@ -116,7 +116,7 @@ const removeAgentIdsFromProject = async function (projectId, agentIds) { * @returns {Promise} */ const removeAgentFromAllProjects = async (agentId) => { - await Project.updateMany({}, { $pull: { agentIds: agentId } }); + await Project.updateMany({}, { $pullAll: { agentIds: [agentId] } }); }; module.exports = { diff --git a/api/models/Prompt.js b/api/models/Prompt.js index 4b14edbc74..b384c06132 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -20,83 +20,25 @@ const { const { PromptGroup, Prompt, AclEntry } = require('~/db/models'); /** - * Create a pipeline for the aggregation to get prompt groups - * @param {Object} query - * @param {number} skip - * @param {number} limit - * @returns {[Object]} - The pipeline for the aggregation + * Batch-fetches production prompts for an array of prompt groups + * and attaches them as `productionPrompt` field. + * Replaces $lookup aggregation for FerretDB compatibility. */ -const createGroupPipeline = (query, skip, limit) => { - return [ - { $match: query }, - { $sort: { createdAt: -1 } }, - { $skip: skip }, - { $limit: limit }, - { - $lookup: { - from: 'prompts', - localField: 'productionId', - foreignField: '_id', - as: 'productionPrompt', - }, - }, - { $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } }, - { - $project: { - name: 1, - numberOfGenerations: 1, - oneliner: 1, - category: 1, - projectIds: 1, - productionId: 1, - author: 1, - authorName: 1, - createdAt: 1, - updatedAt: 1, - 'productionPrompt.prompt': 1, - // 'productionPrompt._id': 1, - // 'productionPrompt.type': 1, - }, - }, - ]; -}; +const attachProductionPrompts = async (groups) => { + const uniqueIds = [...new Set(groups.map((g) => g.productionId?.toString()).filter(Boolean))]; + if (uniqueIds.length === 0) { + return groups.map((g) => ({ ...g, productionPrompt: null })); + } -/** - * Create a pipeline for the aggregation to get all prompt groups - * @param {Object} query - * @param {Partial} $project - * @returns {[Object]} - The pipeline for the aggregation - */ -const createAllGroupsPipeline = ( - query, - $project = { - name: 1, - oneliner: 1, - category: 1, - author: 1, - authorName: 1, - createdAt: 1, - updatedAt: 1, - command: 1, - 'productionPrompt.prompt': 1, - }, -) => { - return [ - { $match: query }, - { $sort: { createdAt: -1 } }, - { - $lookup: { - from: 'prompts', - localField: 'productionId', - foreignField: '_id', - as: 'productionPrompt', - }, - }, - { $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } }, - { - $project, - }, - ]; + const prompts = await Prompt.find({ _id: { $in: uniqueIds } }) + .select('prompt') + .lean(); + const promptMap = new Map(prompts.map((p) => [p._id.toString(), p])); + + return groups.map((g) => ({ + ...g, + productionPrompt: g.productionId ? (promptMap.get(g.productionId.toString()) ?? null) : null, + })); }; /** @@ -137,8 +79,11 @@ const getAllPromptGroups = async (req, filter) => { } } - const promptGroupsPipeline = createAllGroupsPipeline(combinedQuery); - return await PromptGroup.aggregate(promptGroupsPipeline).exec(); + const groups = await PromptGroup.find(combinedQuery) + .sort({ createdAt: -1 }) + .select('name oneliner category author authorName createdAt updatedAt command productionId') + .lean(); + return await attachProductionPrompts(groups); } catch (error) { console.error('Error getting all prompt groups', error); return { message: 'Error getting all prompt groups' }; @@ -178,7 +123,6 @@ const getPromptGroups = async (req, filter) => { let combinedQuery = query; if (searchShared) { - // const projects = req.user.projects || []; // TODO: handle multiple projects const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds'); if (project && project.promptGroupIds && project.promptGroupIds.length > 0) { const projectQuery = { _id: { $in: project.promptGroupIds }, ...query }; @@ -190,17 +134,19 @@ const getPromptGroups = async (req, filter) => { const skip = (validatedPageNumber - 1) * validatedPageSize; const limit = validatedPageSize; - const promptGroupsPipeline = createGroupPipeline(combinedQuery, skip, limit); - const totalPromptGroupsPipeline = [{ $match: combinedQuery }, { $count: 'total' }]; - - const [promptGroupsResults, totalPromptGroupsResults] = await Promise.all([ - PromptGroup.aggregate(promptGroupsPipeline).exec(), - PromptGroup.aggregate(totalPromptGroupsPipeline).exec(), + const [groups, totalPromptGroups] = await Promise.all([ + PromptGroup.find(combinedQuery) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .select( + 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + ) + .lean(), + PromptGroup.countDocuments(combinedQuery), ]); - const promptGroups = promptGroupsResults; - const totalPromptGroups = - totalPromptGroupsResults.length > 0 ? totalPromptGroupsResults[0].total : 0; + const promptGroups = await attachProductionPrompts(groups); return { promptGroups, @@ -268,10 +214,8 @@ async function getListPromptGroupsByAccess({ const isPaginated = limit !== null && limit !== undefined; const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null; - // Build base query combining ACL accessible prompt groups with other filters const baseQuery = { ...otherParams, _id: { $in: accessibleIds } }; - // Add cursor condition if (after && typeof after === 'string' && after !== 'undefined' && after !== 'null') { try { const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8')); @@ -284,10 +228,8 @@ async function getListPromptGroupsByAccess({ ], }; - // Merge cursor condition with base query if (Object.keys(baseQuery).length > 0) { baseQuery.$and = [{ ...baseQuery }, cursorCondition]; - // Remove the original conditions from baseQuery to avoid duplication Object.keys(baseQuery).forEach((key) => { if (key !== '$and') delete baseQuery[key]; }); @@ -299,43 +241,18 @@ async function getListPromptGroupsByAccess({ } } - // Build aggregation pipeline - const pipeline = [{ $match: baseQuery }, { $sort: { updatedAt: -1, _id: 1 } }]; + const findQuery = PromptGroup.find(baseQuery) + .sort({ updatedAt: -1, _id: 1 }) + .select( + 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + ); - // Only apply limit if pagination is requested if (isPaginated) { - pipeline.push({ $limit: normalizedLimit + 1 }); + findQuery.limit(normalizedLimit + 1); } - // Add lookup for production prompt - pipeline.push( - { - $lookup: { - from: 'prompts', - localField: 'productionId', - foreignField: '_id', - as: 'productionPrompt', - }, - }, - { $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } }, - { - $project: { - name: 1, - numberOfGenerations: 1, - oneliner: 1, - category: 1, - projectIds: 1, - productionId: 1, - author: 1, - authorName: 1, - createdAt: 1, - updatedAt: 1, - 'productionPrompt.prompt': 1, - }, - }, - ); - - const promptGroups = await PromptGroup.aggregate(pipeline).exec(); + const groups = await findQuery.lean(); + const promptGroups = await attachProductionPrompts(groups); const hasMore = isPaginated ? promptGroups.length > normalizedLimit : false; const data = (isPaginated ? promptGroups.slice(0, normalizedLimit) : promptGroups).map( @@ -347,7 +264,6 @@ async function getListPromptGroupsByAccess({ }, ); - // Generate next cursor only if paginated let nextCursor = null; if (isPaginated && hasMore && data.length > 0) { const lastGroup = promptGroups[normalizedLimit - 1]; @@ -480,32 +396,33 @@ module.exports = { */ getRandomPromptGroups: async (filter) => { try { - const result = await PromptGroup.aggregate([ - { - $match: { - category: { $ne: '' }, - }, - }, - { - $group: { - _id: '$category', - promptGroup: { $first: '$$ROOT' }, - }, - }, - { - $replaceRoot: { newRoot: '$promptGroup' }, - }, - { - $sample: { size: +filter.limit + +filter.skip }, - }, - { - $skip: +filter.skip, - }, - { - $limit: +filter.limit, - }, - ]); - return { prompts: result }; + const categories = await PromptGroup.distinct('category', { category: { $ne: '' } }); + + for (let i = categories.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [categories[i], categories[j]] = [categories[j], categories[i]]; + } + + const skip = +filter.skip; + const limit = +filter.limit; + const selectedCategories = categories.slice(skip, skip + limit); + + if (selectedCategories.length === 0) { + return { prompts: [] }; + } + + const groups = await PromptGroup.find({ category: { $in: selectedCategories } }).lean(); + + const groupByCategory = new Map(); + for (const group of groups) { + if (!groupByCategory.has(group.category)) { + groupByCategory.set(group.category, group); + } + } + + const prompts = selectedCategories.map((cat) => groupByCategory.get(cat)).filter(Boolean); + + return { prompts }; } catch (error) { logger.error('Error getting prompt groups', error); return { message: 'Error getting prompt groups' }; @@ -656,7 +573,7 @@ module.exports = { await removeGroupIdsFromProject(projectId, [filter._id]); } - updateOps.$pull = { projectIds: { $in: data.removeProjectIds } }; + updateOps.$pullAll = { projectIds: data.removeProjectIds }; delete data.removeProjectIds; } diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 51f6d218ec..48f34479cd 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -365,11 +365,7 @@ const deleteUserController = async (req, res) => { await deleteUserMcpServers(user.id); // delete user MCP servers await Action.deleteMany({ user: user.id }); // delete user actions await Token.deleteMany({ userId: user.id }); // delete user OAuth tokens - await Group.updateMany( - // remove user from all groups - { memberIds: user.id }, - { $pull: { memberIds: user.id } }, - ); + await Group.updateMany({ memberIds: user.id }, { $pullAll: { memberIds: [user.id] } }); await AclEntry.deleteMany({ principalId: user._id }); // delete user ACL entries logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`); res.status(200).send({ message: 'User deleted' }); diff --git a/api/server/controllers/UserController.spec.js b/api/server/controllers/UserController.spec.js new file mode 100644 index 0000000000..cf5d971e02 --- /dev/null +++ b/api/server/controllers/UserController.spec.js @@ -0,0 +1,208 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.mock('@librechat/data-schemas', () => { + const actual = jest.requireActual('@librechat/data-schemas'); + return { + ...actual, + logger: { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }, + }; +}); + +jest.mock('~/models', () => ({ + deleteAllUserSessions: jest.fn().mockResolvedValue(undefined), + deleteAllSharedLinks: jest.fn().mockResolvedValue(undefined), + updateUserPlugins: jest.fn(), + deleteUserById: jest.fn().mockResolvedValue(undefined), + deleteMessages: jest.fn().mockResolvedValue(undefined), + deletePresets: jest.fn().mockResolvedValue(undefined), + deleteUserKey: jest.fn().mockResolvedValue(undefined), + deleteConvos: jest.fn().mockResolvedValue(undefined), + deleteFiles: jest.fn().mockResolvedValue(undefined), + updateUser: jest.fn(), + findToken: jest.fn(), + getFiles: jest.fn().mockResolvedValue([]), +})); + +jest.mock('~/server/services/PluginService', () => ({ + updateUserPluginAuth: jest.fn(), + deleteUserPluginAuth: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('~/server/services/AuthService', () => ({ + verifyEmail: jest.fn(), + resendVerificationEmail: jest.fn(), +})); + +jest.mock('~/server/services/Files/S3/crud', () => ({ + needsRefresh: jest.fn(), + getNewS3URL: jest.fn(), +})); + +jest.mock('~/server/services/Files/process', () => ({ + processDeleteRequest: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('~/server/services/Config', () => ({ + getAppConfig: jest.fn().mockResolvedValue({}), + getMCPManager: jest.fn(), + getFlowStateManager: jest.fn(), + getMCPServersRegistry: jest.fn(), +})); + +jest.mock('~/models/ToolCall', () => ({ + deleteToolCalls: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('~/models/Prompt', () => ({ + deleteUserPrompts: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('~/models/Agent', () => ({ + deleteUserAgents: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(), +})); + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +afterEach(async () => { + const collections = mongoose.connection.collections; + for (const key in collections) { + await collections[key].deleteMany({}); + } +}); + +const { deleteUserController } = require('./UserController'); +const { Group } = require('~/db/models'); +const { deleteConvos } = require('~/models'); + +describe('deleteUserController', () => { + const mockRes = { + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return 200 on successful deletion', async () => { + const userId = new mongoose.Types.ObjectId(); + const req = { user: { id: userId.toString(), _id: userId, email: 'test@test.com' } }; + + await deleteUserController(req, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith({ message: 'User deleted' }); + }); + + it('should remove the user from all groups via $pullAll', async () => { + const userId = new mongoose.Types.ObjectId(); + const userIdStr = userId.toString(); + const otherUser = new mongoose.Types.ObjectId().toString(); + + await Group.create([ + { name: 'Group A', memberIds: [userIdStr, otherUser], source: 'local' }, + { name: 'Group B', memberIds: [userIdStr], source: 'local' }, + { name: 'Group C', memberIds: [otherUser], source: 'local' }, + ]); + + const req = { user: { id: userIdStr, _id: userId, email: 'del@test.com' } }; + await deleteUserController(req, mockRes); + + const groups = await Group.find({}).sort({ name: 1 }).lean(); + expect(groups[0].memberIds).toEqual([otherUser]); + expect(groups[1].memberIds).toEqual([]); + expect(groups[2].memberIds).toEqual([otherUser]); + }); + + it('should handle user that exists in no groups', async () => { + const userId = new mongoose.Types.ObjectId(); + await Group.create({ name: 'Empty', memberIds: ['someone-else'], source: 'local' }); + + const req = { user: { id: userId.toString(), _id: userId, email: 'no-groups@test.com' } }; + await deleteUserController(req, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + const group = await Group.findOne({ name: 'Empty' }).lean(); + expect(group.memberIds).toEqual(['someone-else']); + }); + + it('should remove duplicate memberIds if the user appears more than once', async () => { + const userId = new mongoose.Types.ObjectId(); + const userIdStr = userId.toString(); + + await Group.create({ + name: 'Dupes', + memberIds: [userIdStr, 'other', userIdStr], + source: 'local', + }); + + const req = { user: { id: userIdStr, _id: userId, email: 'dupe@test.com' } }; + await deleteUserController(req, mockRes); + + const group = await Group.findOne({ name: 'Dupes' }).lean(); + expect(group.memberIds).toEqual(['other']); + }); + + it('should still succeed when deleteConvos throws', async () => { + const userId = new mongoose.Types.ObjectId(); + deleteConvos.mockRejectedValueOnce(new Error('no convos')); + + const req = { user: { id: userId.toString(), _id: userId, email: 'convos@test.com' } }; + await deleteUserController(req, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith({ message: 'User deleted' }); + }); + + it('should return 500 when a critical operation fails', async () => { + const userId = new mongoose.Types.ObjectId(); + const { deleteMessages } = require('~/models'); + deleteMessages.mockRejectedValueOnce(new Error('db down')); + + const req = { user: { id: userId.toString(), _id: userId, email: 'fail@test.com' } }; + await deleteUserController(req, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.json).toHaveBeenCalledWith({ message: 'Something went wrong.' }); + }); + + it('should use string user.id (not ObjectId user._id) for memberIds removal', async () => { + const userId = new mongoose.Types.ObjectId(); + const userIdStr = userId.toString(); + const otherUser = 'other-user-id'; + + await Group.create({ + name: 'StringCheck', + memberIds: [userIdStr, otherUser], + source: 'local', + }); + + const req = { user: { id: userIdStr, _id: userId, email: 'stringcheck@test.com' } }; + await deleteUserController(req, mockRes); + + const group = await Group.findOne({ name: 'StringCheck' }).lean(); + expect(group.memberIds).toEqual([otherUser]); + expect(group.memberIds).not.toContain(userIdStr); + }); +}); diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index ede4ea416a..959974bc2d 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -559,7 +559,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { const updatedAgent = mockRes.json.mock.calls[0][0]; expect(updatedAgent).toBeDefined(); - // Note: updateAgentProjects requires more setup, so we just verify the handler doesn't crash }); test('should validate tool_resources in updates', async () => { diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index ba1ef68032..c82ee02599 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -541,7 +541,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null) memberIds: user.idOnTheSource, idOnTheSource: { $nin: allGroupIds }, }, - { $pull: { memberIds: user.idOnTheSource } }, + { $pullAll: { memberIds: [user.idOnTheSource] } }, sessionOptions, ); } catch (error) { @@ -793,7 +793,15 @@ const bulkUpdateResourcePermissions = async ({ return results; } catch (error) { if (shouldEndSession && supportsTransactions) { - await localSession.abortTransaction(); + try { + await localSession.abortTransaction(); + } catch (transactionError) { + /** best-effort abort; may fail if commit already succeeded */ + logger.error( + `[PermissionService.bulkUpdateResourcePermissions] Error aborting transaction:`, + transactionError, + ); + } } logger.error(`[PermissionService.bulkUpdateResourcePermissions] Error: ${error.message}`); throw error; diff --git a/api/server/services/PermissionService.spec.js b/api/server/services/PermissionService.spec.js index b41780f345..477b0702b9 100644 --- a/api/server/services/PermissionService.spec.js +++ b/api/server/services/PermissionService.spec.js @@ -9,6 +9,7 @@ const { } = require('librechat-data-provider'); const { bulkUpdateResourcePermissions, + syncUserEntraGroupMemberships, getEffectivePermissions, findAccessibleResources, getAvailableRoles, @@ -26,7 +27,11 @@ jest.mock('@librechat/data-schemas', () => ({ // Mock GraphApiService to prevent config loading issues jest.mock('~/server/services/GraphApiService', () => ({ + entraIdPrincipalFeatureEnabled: jest.fn().mockReturnValue(false), + getUserOwnedEntraGroups: jest.fn().mockResolvedValue([]), + getUserEntraGroups: jest.fn().mockResolvedValue([]), getGroupMembers: jest.fn().mockResolvedValue([]), + getGroupOwners: jest.fn().mockResolvedValue([]), })); // Mock the logger @@ -1933,3 +1938,134 @@ describe('PermissionService', () => { }); }); }); + +describe('syncUserEntraGroupMemberships - $pullAll on Group.memberIds', () => { + const { + entraIdPrincipalFeatureEnabled, + getUserEntraGroups, + } = require('~/server/services/GraphApiService'); + const { Group } = require('~/db/models'); + + const userEntraId = 'entra-user-001'; + const user = { + openidId: 'openid-sub-001', + idOnTheSource: userEntraId, + provider: 'openid', + }; + + beforeEach(async () => { + await Group.deleteMany({}); + entraIdPrincipalFeatureEnabled.mockReturnValue(true); + }); + + afterEach(() => { + entraIdPrincipalFeatureEnabled.mockReturnValue(false); + getUserEntraGroups.mockResolvedValue([]); + }); + + it('should add user to matching Entra groups and remove from non-matching ones', async () => { + await Group.create([ + { name: 'Group A', source: 'entra', idOnTheSource: 'entra-group-a', memberIds: [] }, + { + name: 'Group B', + source: 'entra', + idOnTheSource: 'entra-group-b', + memberIds: [userEntraId], + }, + { + name: 'Group C', + source: 'entra', + idOnTheSource: 'entra-group-c', + memberIds: [userEntraId], + }, + ]); + + getUserEntraGroups.mockResolvedValue(['entra-group-a', 'entra-group-c']); + + await syncUserEntraGroupMemberships(user, 'fake-access-token'); + + const groups = await Group.find({ source: 'entra' }).sort({ name: 1 }).lean(); + expect(groups[0].memberIds).toContain(userEntraId); + expect(groups[1].memberIds).not.toContain(userEntraId); + expect(groups[2].memberIds).toContain(userEntraId); + }); + + it('should not modify groups when API returns empty list (early return)', async () => { + await Group.create([ + { + name: 'Group X', + source: 'entra', + idOnTheSource: 'entra-x', + memberIds: [userEntraId, 'other-user'], + }, + { name: 'Group Y', source: 'entra', idOnTheSource: 'entra-y', memberIds: [userEntraId] }, + ]); + + getUserEntraGroups.mockResolvedValue([]); + + await syncUserEntraGroupMemberships(user, 'fake-token'); + + const groups = await Group.find({ source: 'entra' }).sort({ name: 1 }).lean(); + expect(groups[0].memberIds).toContain(userEntraId); + expect(groups[0].memberIds).toContain('other-user'); + expect(groups[1].memberIds).toContain(userEntraId); + }); + + it('should remove user from groups not in the API response via $pullAll', async () => { + await Group.create([ + { name: 'Keep', source: 'entra', idOnTheSource: 'entra-keep', memberIds: [userEntraId] }, + { + name: 'Remove', + source: 'entra', + idOnTheSource: 'entra-remove', + memberIds: [userEntraId, 'other-user'], + }, + ]); + + getUserEntraGroups.mockResolvedValue(['entra-keep']); + + await syncUserEntraGroupMemberships(user, 'fake-token'); + + const keep = await Group.findOne({ idOnTheSource: 'entra-keep' }).lean(); + const remove = await Group.findOne({ idOnTheSource: 'entra-remove' }).lean(); + expect(keep.memberIds).toContain(userEntraId); + expect(remove.memberIds).not.toContain(userEntraId); + expect(remove.memberIds).toContain('other-user'); + }); + + it('should not modify local groups', async () => { + await Group.create([ + { name: 'Local Group', source: 'local', memberIds: [userEntraId] }, + { + name: 'Entra Group', + source: 'entra', + idOnTheSource: 'entra-only', + memberIds: [userEntraId], + }, + ]); + + getUserEntraGroups.mockResolvedValue([]); + + await syncUserEntraGroupMemberships(user, 'fake-token'); + + const localGroup = await Group.findOne({ source: 'local' }).lean(); + expect(localGroup.memberIds).toContain(userEntraId); + }); + + it('should early-return when feature is disabled', async () => { + entraIdPrincipalFeatureEnabled.mockReturnValue(false); + + await Group.create({ + name: 'Should Not Touch', + source: 'entra', + idOnTheSource: 'entra-safe', + memberIds: [userEntraId], + }); + + getUserEntraGroups.mockResolvedValue([]); + await syncUserEntraGroupMemberships(user, 'fake-token'); + + const group = await Group.findOne({ idOnTheSource: 'entra-safe' }).lean(); + expect(group.memberIds).toContain(userEntraId); + }); +}); diff --git a/config/delete-user.js b/config/delete-user.js index 5ad85577a4..66e325d1ee 100644 --- a/config/delete-user.js +++ b/config/delete-user.js @@ -107,7 +107,7 @@ async function gracefulExit(code = 0) { await Promise.all(tasks); // 6) Remove user from all groups - await Group.updateMany({ memberIds: user._id }, { $pull: { memberIds: user._id } }); + await Group.updateMany({ memberIds: uid }, { $pullAll: { memberIds: [uid] } }); // 7) Finally delete the user document itself await User.deleteOne({ _id: uid }); diff --git a/config/migrate-agent-permissions.js b/config/migrate-agent-permissions.js index b206c648ca..b511fba50f 100644 --- a/config/migrate-agent-permissions.js +++ b/config/migrate-agent-permissions.js @@ -10,7 +10,7 @@ const connect = require('./connect'); const { grantPermission } = require('~/server/services/PermissionService'); const { getProjectByName } = require('~/models/Project'); const { findRoleByIdentifier } = require('~/models'); -const { Agent } = require('~/db/models'); +const { Agent, AclEntry } = require('~/db/models'); async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 } = {}) { await connect(); @@ -39,48 +39,17 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 logger.info(`Found ${globalAgentIds.size} agents in global project`); - // Find agents without ACL entries using DocumentDB-compatible approach - const agentsToMigrate = await Agent.aggregate([ - { - $lookup: { - from: 'aclentries', - localField: '_id', - foreignField: 'resourceId', - as: 'aclEntries', - }, - }, - { - $addFields: { - userAclEntries: { - $filter: { - input: '$aclEntries', - as: 'aclEntry', - cond: { - $and: [ - { $eq: ['$$aclEntry.resourceType', ResourceType.AGENT] }, - { $eq: ['$$aclEntry.principalType', PrincipalType.USER] }, - ], - }, - }, - }, - }, - }, - { - $match: { - author: { $exists: true, $ne: null }, - userAclEntries: { $size: 0 }, - }, - }, - { - $project: { - _id: 1, - id: 1, - name: 1, - author: 1, - isCollaborative: 1, - }, - }, - ]); + const migratedAgentIds = await AclEntry.distinct('resourceId', { + resourceType: ResourceType.AGENT, + principalType: PrincipalType.USER, + }); + + const agentsToMigrate = await Agent.find({ + _id: { $nin: migratedAgentIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id id name author isCollaborative') + .lean(); const categories = { globalEditAccess: [], // Global project + collaborative -> Public EDIT diff --git a/config/migrate-prompt-permissions.js b/config/migrate-prompt-permissions.js index 6018b16631..d86ee92f08 100644 --- a/config/migrate-prompt-permissions.js +++ b/config/migrate-prompt-permissions.js @@ -10,7 +10,7 @@ const connect = require('./connect'); const { grantPermission } = require('~/server/services/PermissionService'); const { getProjectByName } = require('~/models/Project'); const { findRoleByIdentifier } = require('~/models'); -const { PromptGroup } = require('~/db/models'); +const { PromptGroup, AclEntry } = require('~/db/models'); async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 } = {}) { await connect(); @@ -41,48 +41,17 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 logger.info(`Found ${globalPromptGroupIds.size} prompt groups in global project`); - // Find promptGroups without ACL entries - const promptGroupsToMigrate = await PromptGroup.aggregate([ - { - $lookup: { - from: 'aclentries', - localField: '_id', - foreignField: 'resourceId', - as: 'aclEntries', - }, - }, - { - $addFields: { - promptGroupAclEntries: { - $filter: { - input: '$aclEntries', - as: 'aclEntry', - cond: { - $and: [ - { $eq: ['$$aclEntry.resourceType', ResourceType.PROMPTGROUP] }, - { $eq: ['$$aclEntry.principalType', PrincipalType.USER] }, - ], - }, - }, - }, - }, - }, - { - $match: { - author: { $exists: true, $ne: null }, - promptGroupAclEntries: { $size: 0 }, - }, - }, - { - $project: { - _id: 1, - name: 1, - author: 1, - authorName: 1, - category: 1, - }, - }, - ]); + const migratedGroupIds = await AclEntry.distinct('resourceId', { + resourceType: ResourceType.PROMPTGROUP, + principalType: PrincipalType.USER, + }); + + const promptGroupsToMigrate = await PromptGroup.find({ + _id: { $nin: migratedGroupIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id name author authorName category') + .lean(); const categories = { globalViewAccess: [], // PromptGroup in global project -> Public VIEW diff --git a/e2e/setup/cleanupUser.ts b/e2e/setup/cleanupUser.ts index 20ad661a5d..2e3de7d735 100644 --- a/e2e/setup/cleanupUser.ts +++ b/e2e/setup/cleanupUser.ts @@ -46,7 +46,8 @@ export default async function cleanupUser(user: TUser) { await Transaction.deleteMany({ user: userId }); await Token.deleteMany({ userId: userId }); await AclEntry.deleteMany({ principalId: userId }); - await Group.updateMany({ memberIds: userId }, { $pull: { memberIds: userId } }); + const userIdStr = userId.toString(); + await Group.updateMany({ memberIds: userIdStr }, { $pullAll: { memberIds: [userIdStr] } }); await User.deleteMany({ _id: userId }); console.log('🤖: ✅ Deleted user from Database'); diff --git a/eslint.config.mjs b/eslint.config.mjs index f53c4e83dd..bd848c7e3e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -39,6 +39,7 @@ export default [ 'packages/data-provider/dist/**/*', 'packages/data-provider/test_bundle/**/*', 'packages/data-schemas/dist/**/*', + 'packages/data-schemas/misc/**/*', 'data-node/**/*', 'meili_data/**/*', '**/node_modules/**/*', diff --git a/packages/api/src/agents/migration.ts b/packages/api/src/agents/migration.ts index f8cad88b66..4da3852f82 100644 --- a/packages/api/src/agents/migration.ts +++ b/packages/api/src/agents/migration.ts @@ -24,7 +24,7 @@ export interface MigrationCheckParams { } interface AgentMigrationData { - _id: string; + _id: unknown; id: string; name: string; author: string; @@ -81,48 +81,18 @@ export async function checkAgentPermissionsMigration({ const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']); const globalAgentIds = new Set(globalProject?.agentIds || []); - // Find agents without ACL entries (no batching for efficiency on startup) - const agentsToMigrate: AgentMigrationData[] = await AgentModel.aggregate([ - { - $lookup: { - from: 'aclentries', - localField: '_id', - foreignField: 'resourceId', - as: 'aclEntries', - }, - }, - { - $addFields: { - userAclEntries: { - $filter: { - input: '$aclEntries', - as: 'aclEntry', - cond: { - $and: [ - { $eq: ['$$aclEntry.resourceType', ResourceType.AGENT] }, - { $eq: ['$$aclEntry.principalType', PrincipalType.USER] }, - ], - }, - }, - }, - }, - }, - { - $match: { - author: { $exists: true, $ne: null }, - userAclEntries: { $size: 0 }, - }, - }, - { - $project: { - _id: 1, - id: 1, - name: 1, - author: 1, - isCollaborative: 1, - }, - }, - ]); + const AclEntry = mongoose.model('AclEntry'); + const migratedAgentIds = await AclEntry.distinct('resourceId', { + resourceType: ResourceType.AGENT, + principalType: PrincipalType.USER, + }); + + const agentsToMigrate = (await AgentModel.find({ + _id: { $nin: migratedAgentIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id id name author isCollaborative') + .lean()) as unknown as AgentMigrationData[]; const categories: { globalEditAccess: AgentMigrationData[]; diff --git a/packages/api/src/prompts/migration.ts b/packages/api/src/prompts/migration.ts index 40f0a585d7..a9e71d427a 100644 --- a/packages/api/src/prompts/migration.ts +++ b/packages/api/src/prompts/migration.ts @@ -24,7 +24,7 @@ export interface PromptMigrationCheckParams { } interface PromptGroupMigrationData { - _id: string; + _id: { toString(): string }; name: string; author: string; authorName?: string; @@ -81,48 +81,18 @@ export async function checkPromptPermissionsMigration({ (globalProject?.promptGroupIds || []).map((id) => id.toString()), ); - // Find promptGroups without ACL entries (no batching for efficiency on startup) - const promptGroupsToMigrate: PromptGroupMigrationData[] = await PromptGroupModel.aggregate([ - { - $lookup: { - from: 'aclentries', - localField: '_id', - foreignField: 'resourceId', - as: 'aclEntries', - }, - }, - { - $addFields: { - promptGroupAclEntries: { - $filter: { - input: '$aclEntries', - as: 'aclEntry', - cond: { - $and: [ - { $eq: ['$$aclEntry.resourceType', ResourceType.PROMPTGROUP] }, - { $eq: ['$$aclEntry.principalType', PrincipalType.USER] }, - ], - }, - }, - }, - }, - }, - { - $match: { - author: { $exists: true, $ne: null }, - promptGroupAclEntries: { $size: 0 }, - }, - }, - { - $project: { - _id: 1, - name: 1, - author: 1, - authorName: 1, - category: 1, - }, - }, - ]); + const AclEntry = mongoose.model('AclEntry'); + const migratedGroupIds = await AclEntry.distinct('resourceId', { + resourceType: ResourceType.PROMPTGROUP, + principalType: PrincipalType.USER, + }); + + const promptGroupsToMigrate = (await PromptGroupModel.find({ + _id: { $nin: migratedGroupIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id name author authorName category') + .lean()) as unknown as PromptGroupMigrationData[]; const categories: { globalViewAccess: PromptGroupMigrationData[]; diff --git a/packages/data-schemas/jest.config.mjs b/packages/data-schemas/jest.config.mjs index 19d392f368..800143d679 100644 --- a/packages/data-schemas/jest.config.mjs +++ b/packages/data-schemas/jest.config.mjs @@ -1,6 +1,7 @@ export default { collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!/node_modules/'], coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], + testPathIgnorePatterns: ['/node_modules/', '/dist/', '/misc/'], coverageReporters: ['text', 'cobertura'], testResultsProcessor: 'jest-junit', moduleNameMapper: { diff --git a/packages/data-schemas/misc/ferretdb/aclBitops.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/aclBitops.ferretdb.spec.ts new file mode 100644 index 0000000000..d8fb4ec84b --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/aclBitops.ferretdb.spec.ts @@ -0,0 +1,468 @@ +import mongoose from 'mongoose'; +import { ResourceType, PrincipalType, PermissionBits } from 'librechat-data-provider'; +import type * as t from '~/types'; +import { createAclEntryMethods } from '~/methods/aclEntry'; +import aclEntrySchema from '~/schema/aclEntry'; + +/** + * Integration tests for $bit and $bitsAllSet on FerretDB. + * + * Validates that modifyPermissionBits (using atomic $bit) + * and $bitsAllSet queries work identically on both MongoDB and FerretDB. + * + * Run against FerretDB: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/aclbit_test" npx jest aclBitops.ferretdb + * + * Run against MongoDB (for parity): + * FERRETDB_URI="mongodb://127.0.0.1:27017/aclbit_test" npx jest aclBitops.ferretdb + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +describeIfFerretDB('ACL bitwise operations - FerretDB compatibility', () => { + let AclEntry: mongoose.Model; + let methods: ReturnType; + + const userId = new mongoose.Types.ObjectId(); + const groupId = new mongoose.Types.ObjectId(); + const grantedById = new mongoose.Types.ObjectId(); + + beforeAll(async () => { + await mongoose.connect(FERRETDB_URI as string); + AclEntry = mongoose.models.AclEntry || mongoose.model('AclEntry', aclEntrySchema); + methods = createAclEntryMethods(mongoose); + await AclEntry.createCollection(); + }); + + afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.disconnect(); + }); + + afterEach(async () => { + await AclEntry.deleteMany({}); + }); + + describe('modifyPermissionBits (atomic $bit operator)', () => { + it('should add permission bits to existing entry', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + null, + ); + + expect(updated).toBeDefined(); + expect(updated?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); + }); + + it('should remove permission bits from existing entry', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + null, + PermissionBits.EDIT, + ); + + expect(updated).toBeDefined(); + expect(updated?.permBits).toBe(PermissionBits.VIEW | PermissionBits.DELETE); + }); + + it('should add and remove bits in one operation', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT | PermissionBits.DELETE, + PermissionBits.VIEW, + ); + + expect(updated).toBeDefined(); + expect(updated?.permBits).toBe(PermissionBits.EDIT | PermissionBits.DELETE); + }); + + it('should handle adding bits that are already set (idempotent OR)', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW | PermissionBits.EDIT, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + null, + ); + + expect(updated?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); + }); + + it('should handle removing bits that are not set (no-op AND)', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + null, + PermissionBits.DELETE, + ); + + expect(updated?.permBits).toBe(PermissionBits.VIEW); + }); + + it('should handle all four permission bits', async () => { + const resourceId = new mongoose.Types.ObjectId(); + const allBits = + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE; + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + allBits, + grantedById, + ); + + const afterRemove = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + null, + PermissionBits.EDIT | PermissionBits.SHARE, + ); + + expect(afterRemove?.permBits).toBe(PermissionBits.VIEW | PermissionBits.DELETE); + }); + + it('should work with group principals', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.GROUP, + groupId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.GROUP, + groupId, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + null, + ); + + expect(updated?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); + }); + + it('should work with public principals', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW | PermissionBits.EDIT, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.PUBLIC, + null, + ResourceType.AGENT, + resourceId, + null, + PermissionBits.EDIT, + ); + + expect(updated?.permBits).toBe(PermissionBits.VIEW); + }); + + it('should return null when entry does not exist', async () => { + const nonexistentResource = new mongoose.Types.ObjectId(); + + const result = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + nonexistentResource, + PermissionBits.EDIT, + null, + ); + + expect(result).toBeNull(); + }); + + it('should clear all bits via remove', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW | PermissionBits.EDIT, + grantedById, + ); + + const updated = await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + null, + PermissionBits.VIEW | PermissionBits.EDIT, + ); + + expect(updated?.permBits).toBe(0); + }); + }); + + describe('$bitsAllSet queries (hasPermission + findAccessibleResources)', () => { + it('should find entries with specific bits set via hasPermission', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW | PermissionBits.EDIT, + grantedById, + ); + + const principals = [{ principalType: PrincipalType.USER, principalId: userId }]; + + expect( + await methods.hasPermission( + principals, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + ), + ).toBe(true); + expect( + await methods.hasPermission( + principals, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + ), + ).toBe(true); + expect( + await methods.hasPermission( + principals, + ResourceType.AGENT, + resourceId, + PermissionBits.DELETE, + ), + ).toBe(false); + }); + + it('should find accessible resources filtered by permission bit', async () => { + const res1 = new mongoose.Types.ObjectId(); + const res2 = new mongoose.Types.ObjectId(); + const res3 = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + res1, + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + res2, + PermissionBits.VIEW | PermissionBits.EDIT, + grantedById, + ); + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + res3, + PermissionBits.EDIT, + grantedById, + ); + + const principals = [{ principalType: PrincipalType.USER, principalId: userId }]; + + const viewable = await methods.findAccessibleResources( + principals, + ResourceType.AGENT, + PermissionBits.VIEW, + ); + expect(viewable.map((r) => r.toString()).sort()).toEqual( + [res1.toString(), res2.toString()].sort(), + ); + + const editable = await methods.findAccessibleResources( + principals, + ResourceType.AGENT, + PermissionBits.EDIT, + ); + expect(editable.map((r) => r.toString()).sort()).toEqual( + [res2.toString(), res3.toString()].sort(), + ); + }); + + it('should correctly query after modifyPermissionBits changes', async () => { + const resourceId = new mongoose.Types.ObjectId(); + const principals = [{ principalType: PrincipalType.USER, principalId: userId }]; + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + + expect( + await methods.hasPermission( + principals, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + ), + ).toBe(true); + expect( + await methods.hasPermission( + principals, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + ), + ).toBe(false); + + await methods.modifyPermissionBits( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + PermissionBits.VIEW, + ); + + expect( + await methods.hasPermission( + principals, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + ), + ).toBe(false); + expect( + await methods.hasPermission( + principals, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + ), + ).toBe(true); + }); + + it('should combine effective permissions across user and group', async () => { + const resourceId = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.GROUP, + groupId, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + grantedById, + ); + + const principals = [ + { principalType: PrincipalType.USER, principalId: userId }, + { principalType: PrincipalType.GROUP, principalId: groupId }, + ]; + + const effective = await methods.getEffectivePermissions( + principals, + ResourceType.AGENT, + resourceId, + ); + + expect(effective).toBe(PermissionBits.VIEW | PermissionBits.EDIT); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/docker-compose.ferretdb.yml b/packages/data-schemas/misc/ferretdb/docker-compose.ferretdb.yml new file mode 100644 index 0000000000..83b6ae7ced --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/docker-compose.ferretdb.yml @@ -0,0 +1,21 @@ +services: + ferretdb-postgres: + image: ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0 + restart: on-failure + environment: + - POSTGRES_USER=ferretdb + - POSTGRES_PASSWORD=ferretdb + - POSTGRES_DB=postgres + volumes: + - ferretdb_data:/var/lib/postgresql/data + + ferretdb: + image: ghcr.io/ferretdb/ferretdb:2.7.0 + restart: on-failure + ports: + - "27020:27017" + environment: + - FERRETDB_POSTGRESQL_URL=postgres://ferretdb:ferretdb@ferretdb-postgres:5432/postgres + +volumes: + ferretdb_data: diff --git a/packages/data-schemas/misc/ferretdb/ferretdb-multitenancy-plan.md b/packages/data-schemas/misc/ferretdb/ferretdb-multitenancy-plan.md new file mode 100644 index 0000000000..5e2569d087 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/ferretdb-multitenancy-plan.md @@ -0,0 +1,204 @@ +# FerretDB Multi-Tenancy Plan + +## Status: Active Investigation + +## Goal + +Database-per-org data isolation using FerretDB (PostgreSQL-backed) with horizontal sharding across multiple FerretDB+Postgres pairs. MongoDB and AWS DocumentDB are not options. + +--- + +## Findings + +### 1. FerretDB Architecture (DocumentDB Backend) + +FerretDB with `postgres-documentdb` does **not** create separate PostgreSQL schemas per MongoDB database. All data lives in a single `documentdb_data` PG schema: + +- Each MongoDB collection → `documents_` + `retry_` table pair +- Catalog tracked in `documentdb_api_catalog.collections` and `.collection_indexes` +- `mongoose.connection.useDb('org_X')` creates a logical database in DocumentDB's catalog + +**Implication**: No PG-level schema isolation, but logical isolation is enforced by FerretDB's wire protocol layer. Backup/restore must go through FerretDB, not raw `pg_dump`. + +### 2. Schema & Index Compatibility + +All 29 LibreChat Mongoose models and 98 custom indexes work on FerretDB v2.7.0: + +| Index Type | Count | Status | +|---|---|---| +| Sparse + unique | 9 (User OAuth IDs) | Working | +| TTL (expireAfterSeconds) | 8 models | Working | +| partialFilterExpression | 2 (File, Group) | Working | +| Compound unique | 5+ | Working | +| Concurrent creation | All 29 models | No deadlock (single org) | + +### 3. Scaling Curve (Empirically Tested) + +| Orgs | Collections | Catalog Indexes | Data Tables | pg_class | Init/org | Query avg | Query p95 | +|------|-------------|-----------------|-------------|----------|----------|-----------|-----------| +| 10 | 450 | 1,920 | 900 | 5,975 | 501ms | 1.03ms | 1.44ms | +| 50 | 1,650 | 7,040 | 3,300 | 20,695 | 485ms | 1.00ms | 1.46ms | +| 100 | 3,150 | 13,440 | 6,300 | 39,095 | 483ms | 0.83ms | 1.13ms | + +**Key finding**: Init time and query latency are flat through 100 orgs. No degradation. + +### 4. Write Amplification + +User model (11+ indexes) vs zero-index collection: **1.11x** — only 11% overhead. DocumentDB's JSONB index management is efficient. + +### 5. Sharding PoC + +Tenant router proven with: +- Pool assignment with capacity limits (fill-then-spill) +- Warm cache routing overhead: **0.001ms** (sub-microsecond) +- Cold routing (DB lookup + connection + model registration): **6ms** +- Cross-pool data isolation confirmed +- Express middleware pattern (`req.getModel('User')`) works transparently + +### 6. Scaling Thresholds + +| Org Count | Postgres Instances | Notes | +|-----------|-------------------|-------| +| 1–300 | 1 | Default config | +| 300–700 | 1 | Tune autovacuum, PgBouncer, shared_buffers | +| 700–1,000 | 1-2 | Split when monitoring signals pressure | +| 1,000+ | N / ~500 each | One FerretDB+Postgres pair per ~500 orgs | + +### 7. Deadlock Behavior + +- **Single org, concurrent index creation**: No deadlock (DocumentDB handles it) +- **Bulk provisioning (10 orgs sequential)**: Deadlock occurred on Pool B, recovered via retry +- **Production requirement**: Exponential backoff + jitter retry on `createIndexes()` + +--- + +## Open Items + +### A. Production Deadlock Retry ✅ +- [x] Build `retryWithBackoff` utility with exponential backoff + jitter +- [x] Integrate into `initializeOrgCollections` and `migrateOrg` scripts +- [x] Tested against FerretDB — real deadlocks detected and recovered: + - `retry_4` hit a deadlock on `createIndexes(User)`, recovered via backoff (1,839ms total) + - `retry_5` also hit retry path (994ms vs ~170ms clean) + - Production utility at `packages/data-schemas/src/utils/retryWithBackoff.ts` + +### B. Per-Org Backup/Restore ✅ +- [x] `mongodump`/`mongorestore` CLI not available — tested programmatic driver-level approach +- [x] **Backup**: `listCollections()` → `find({}).toArray()` per collection → in-memory `OrgBackup` struct +- [x] **Restore**: `collection.insertMany(docs)` per collection into fresh org database +- [x] **BSON type preservation verified**: ObjectId, Date, String all round-trip correctly +- [x] **Data integrity verified**: `_id` values, field values, document counts match exactly +- [x] **Performance**: Backup 24ms, Restore 15ms (8 docs across 29 collections) +- [x] Scales linearly with document count — no per-collection overhead beyond the query + +### C. Schema Migration Across Orgs ✅ +- [x] `createIndexes()` is idempotent — re-init took 86ms with 12 indexes unchanged +- [x] **New collection propagation**: Added `AuditLog` collection with 4 indexes to 5 orgs — 109ms total +- [x] **New index propagation**: Added compound `{username:1, createdAt:-1}` index to `users` across 5 orgs — 22ms total +- [x] **Full migration run**: 5 orgs × 29 models = 88ms/org average (with deadlock retry) +- [x] **Data preservation confirmed**: All existing user data intact after migration +- [x] Extrapolating: 1,000 orgs × 88ms/org = ~88 seconds for a full migration sweep + +--- + +## Test Files + +| File | Purpose | +|---|---| +| `packages/data-schemas/src/methods/multiTenancy.ferretdb.spec.ts` | 5-phase benchmark (useDb mapping, indexes, scaling, write amp, shared collection) | +| `packages/data-schemas/src/methods/sharding.ferretdb.spec.ts` | Sharding PoC (router, assignment, isolation, middleware pattern) | +| `packages/data-schemas/src/methods/orgOperations.ferretdb.spec.ts` | Production operations (backup/restore, migration, deadlock retry) | +| `packages/data-schemas/src/utils/retryWithBackoff.ts` | Production retry utility | + +## Docker + +| File | Purpose | +|---|---| +| `docker-compose.ferretdb.yml` | Single FerretDB + Postgres (dev/test) | + +--- + +## Detailed Empirical Results + +### Deadlock Retry Behavior + +The `retryWithBackoff` utility was exercised under real FerretDB load. Key observations: + +| Scenario | Attempts | Total Time | Notes | +|---|---|---|---| +| Clean org init (no contention) | 1 | 165-199ms | Most orgs complete in one shot | +| Deadlock on User indexes | 2 | 994ms | Single retry recovers cleanly | +| Deadlock with compounding retries | 2-3 | 1,839ms | Worst case in 5-org sequential batch | + +The `User` model (11+ indexes including 9 sparse unique) is the most deadlock-prone collection. The retry utility's exponential backoff with jitter (100ms base, 10s cap) handles this gracefully. + +### Backup/Restore Round-Trip + +Tested with a realistic org containing 4 populated collections: + +| Operation | Time | Details | +|---|---|---| +| Backup (full org) | 24ms | 8 docs across 29 collections (25 empty) | +| Restore (to new org) | 15ms | Including `insertMany()` for each collection | +| Index re-creation | ~500ms | Separate `initializeOrgCollections` call | + +Round-trip verified: +- `_id` (ObjectId) preserved exactly +- `createdAt` / `updatedAt` (Date) preserved +- String, Number, ObjectId ref fields preserved +- Document counts match source + +For larger orgs (thousands of messages/conversations), backup time scales linearly with document count. The bottleneck is network I/O to FerretDB, not serialization. + +### Schema Migration Performance + +| Operation | Time | Per Org | +|---|---|---| +| Idempotent re-init (no changes) | 86ms | 86ms | +| New collection + 4 indexes | 109ms | 22ms/org | +| New compound index on users | 22ms | 4.4ms/org | +| Full migration sweep (29 models) | 439ms | 88ms/org | + +Migration is safe to run while the app is serving traffic — `createIndexes` and `createCollection` are non-blocking operations that don't lock existing data. + +### 5-Org Provisioning with Production Retry + +``` +retry_1: 193ms (29 models) — clean +retry_2: 199ms (29 models) — clean +retry_3: 165ms (29 models) — clean +retry_4: 1839ms (29 models) — deadlock on User indexes, recovered +retry_5: 994ms (29 models) — deadlock on User indexes, recovered +Total: 3,390ms for 5 orgs (678ms avg, but 165ms median) +``` + +--- + +## Production Recommendations + +### 1. Org Provisioning + +Use `initializeOrgCollections()` from `packages/data-schemas/src/utils/retryWithBackoff.ts` for all new org setup. Process orgs in batches of 10 with `Promise.all()` to parallelize across pools while minimizing per-pool contention. + +### 2. Backup Strategy + +Implement driver-level backup (not `mongodump`): +- Enumerate collections via `listCollections()` +- Stream documents via `find({}).batchSize(1000)` for large collections +- Write to object storage (S3/GCS) as NDJSON per collection +- Restore via `insertMany()` in batches of 1,000 + +### 3. Schema Migrations + +Run `migrateAllOrgs()` as a deployment step: +- Enumerate all org databases from the assignment table +- For each org: register models, `createCollection()`, `createIndexesWithRetry()` +- `createIndexes()` is idempotent — safe to re-run +- At 88ms/org, 1,000 orgs complete in ~90 seconds + +### 4. Monitoring + +Track per-org provisioning and migration times. If the median provisioning time rises above 500ms/org, investigate PostgreSQL catalog pressure: +- `pg_stat_user_tables.n_dead_tup` for autovacuum health +- `pg_stat_bgwriter.buffers_backend` for buffer pressure +- `documentdb_api_catalog.collections` count for total table count diff --git a/packages/data-schemas/misc/ferretdb/jest.ferretdb.config.mjs b/packages/data-schemas/misc/ferretdb/jest.ferretdb.config.mjs new file mode 100644 index 0000000000..b5477be737 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/jest.ferretdb.config.mjs @@ -0,0 +1,18 @@ +/** + * Jest config for FerretDB integration tests. + * These tests require a running FerretDB instance and are NOT run in CI. + * + * Usage: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/test_db" \ + * npx jest --config misc/ferretdb/jest.ferretdb.config.mjs --testTimeout=300000 [pattern] + */ +export default { + rootDir: '../..', + testMatch: ['/misc/ferretdb/**/*.ferretdb.spec.ts'], + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + '^~/(.*)$': '/src/$1', + }, + restoreMocks: true, + testTimeout: 300000, +}; diff --git a/packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts new file mode 100644 index 0000000000..f2561137b7 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts @@ -0,0 +1,362 @@ +import mongoose, { Schema, Types } from 'mongoose'; + +/** + * Integration tests for migration anti-join → $nin replacement. + * + * The original migration scripts used a $lookup + $filter + $match({ $size: 0 }) + * anti-join to find resources without ACL entries. FerretDB does not support + * $lookup, so this was replaced with a two-step pattern: + * 1. AclEntry.distinct('resourceId', { resourceType, principalType }) + * 2. Model.find({ _id: { $nin: migratedIds }, ... }) + * + * Run against FerretDB: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/migration_antijoin_test" npx jest migrationAntiJoin.ferretdb + * + * Run against MongoDB (for parity): + * FERRETDB_URI="mongodb://127.0.0.1:27017/migration_antijoin_test" npx jest migrationAntiJoin.ferretdb + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; + +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const agentSchema = new Schema({ + id: { type: String, required: true }, + name: { type: String, required: true }, + author: { type: String }, + isCollaborative: { type: Boolean, default: false }, +}); + +const promptGroupSchema = new Schema({ + name: { type: String, required: true }, + author: { type: String }, + authorName: { type: String }, + category: { type: String }, +}); + +const aclEntrySchema = new Schema( + { + principalType: { type: String, required: true }, + principalId: { type: Schema.Types.Mixed }, + resourceType: { type: String, required: true }, + resourceId: { type: Schema.Types.ObjectId, required: true }, + permBits: { type: Number, default: 1 }, + roleId: { type: Schema.Types.ObjectId }, + grantedBy: { type: Schema.Types.ObjectId }, + grantedAt: { type: Date, default: Date.now }, + }, + { timestamps: true }, +); + +type AgentDoc = mongoose.InferSchemaType; +type PromptGroupDoc = mongoose.InferSchemaType; +type AclEntryDoc = mongoose.InferSchemaType; + +describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () => { + let Agent: mongoose.Model; + let PromptGroup: mongoose.Model; + let AclEntry: mongoose.Model; + + beforeAll(async () => { + await mongoose.connect(FERRETDB_URI as string); + Agent = mongoose.model('TestMigAgent', agentSchema); + PromptGroup = mongoose.model('TestMigPromptGroup', promptGroupSchema); + AclEntry = mongoose.model('TestMigAclEntry', aclEntrySchema); + }); + + afterAll(async () => { + await mongoose.connection.db?.dropDatabase(); + await mongoose.disconnect(); + }); + + beforeEach(async () => { + await Agent.deleteMany({}); + await PromptGroup.deleteMany({}); + await AclEntry.deleteMany({}); + }); + + describe('agent migration pattern', () => { + it('should return only agents WITHOUT user-type ACL entries', async () => { + const agent1 = await Agent.create({ id: 'agent_1', name: 'Migrated Agent', author: 'user1' }); + const agent2 = await Agent.create({ + id: 'agent_2', + name: 'Unmigrated Agent', + author: 'user2', + }); + await Agent.create({ id: 'agent_3', name: 'Another Unmigrated', author: 'user3' }); + + await AclEntry.create({ + principalType: 'user', + principalId: new Types.ObjectId(), + resourceType: 'agent', + resourceId: agent1._id, + }); + + await AclEntry.create({ + principalType: 'public', + resourceType: 'agent', + resourceId: agent2._id, + }); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'agent', + principalType: 'user', + }); + + const toMigrate = await Agent.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id id name author isCollaborative') + .lean(); + + expect(toMigrate).toHaveLength(2); + const names = toMigrate.map((a: Record) => a.name).sort(); + expect(names).toEqual(['Another Unmigrated', 'Unmigrated Agent']); + }); + + it('should exclude agents without an author', async () => { + await Agent.create({ id: 'agent_no_author', name: 'No Author' }); + await Agent.create({ id: 'agent_null_author', name: 'Null Author', author: null }); + await Agent.create({ id: 'agent_with_author', name: 'Has Author', author: 'user1' }); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'agent', + principalType: 'user', + }); + + const toMigrate = await Agent.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id id name author') + .lean(); + + expect(toMigrate).toHaveLength(1); + expect((toMigrate[0] as Record).name).toBe('Has Author'); + }); + + it('should return empty array when all agents are migrated', async () => { + const agent1 = await Agent.create({ id: 'a1', name: 'Agent 1', author: 'user1' }); + const agent2 = await Agent.create({ id: 'a2', name: 'Agent 2', author: 'user2' }); + + await AclEntry.create([ + { + principalType: 'user', + principalId: new Types.ObjectId(), + resourceType: 'agent', + resourceId: agent1._id, + }, + { + principalType: 'user', + principalId: new Types.ObjectId(), + resourceType: 'agent', + resourceId: agent2._id, + }, + ]); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'agent', + principalType: 'user', + }); + + const toMigrate = await Agent.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }).lean(); + + expect(toMigrate).toHaveLength(0); + }); + + it('should not be confused by ACL entries for a different resourceType', async () => { + const agent = await Agent.create({ id: 'a1', name: 'Agent', author: 'user1' }); + + await AclEntry.create({ + principalType: 'user', + principalId: new Types.ObjectId(), + resourceType: 'promptGroup', + resourceId: agent._id, + }); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'agent', + principalType: 'user', + }); + + const toMigrate = await Agent.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }).lean(); + + expect(toMigrate).toHaveLength(1); + expect((toMigrate[0] as Record).name).toBe('Agent'); + }); + + it('should return correct projected fields', async () => { + await Agent.create({ + id: 'proj_agent', + name: 'Field Test', + author: 'user1', + isCollaborative: true, + }); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'agent', + principalType: 'user', + }); + + const toMigrate = await Agent.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id id name author isCollaborative') + .lean(); + + expect(toMigrate).toHaveLength(1); + const agent = toMigrate[0] as Record; + expect(agent).toHaveProperty('_id'); + expect(agent).toHaveProperty('id', 'proj_agent'); + expect(agent).toHaveProperty('name', 'Field Test'); + expect(agent).toHaveProperty('author', 'user1'); + expect(agent).toHaveProperty('isCollaborative', true); + }); + }); + + describe('promptGroup migration pattern', () => { + it('should return only prompt groups WITHOUT user-type ACL entries', async () => { + const pg1 = await PromptGroup.create({ + name: 'Migrated PG', + author: 'user1', + category: 'code', + }); + await PromptGroup.create({ name: 'Unmigrated PG', author: 'user2', category: 'writing' }); + + await AclEntry.create({ + principalType: 'user', + principalId: new Types.ObjectId(), + resourceType: 'promptGroup', + resourceId: pg1._id, + }); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'promptGroup', + principalType: 'user', + }); + + const toMigrate = await PromptGroup.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id name author authorName category') + .lean(); + + expect(toMigrate).toHaveLength(1); + expect((toMigrate[0] as Record).name).toBe('Unmigrated PG'); + }); + + it('should return correct projected fields for prompt groups', async () => { + await PromptGroup.create({ + name: 'PG Fields', + author: 'user1', + authorName: 'Test User', + category: 'marketing', + }); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'promptGroup', + principalType: 'user', + }); + + const toMigrate = await PromptGroup.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }) + .select('_id name author authorName category') + .lean(); + + expect(toMigrate).toHaveLength(1); + const pg = toMigrate[0] as Record; + expect(pg).toHaveProperty('_id'); + expect(pg).toHaveProperty('name', 'PG Fields'); + expect(pg).toHaveProperty('author', 'user1'); + expect(pg).toHaveProperty('authorName', 'Test User'); + expect(pg).toHaveProperty('category', 'marketing'); + }); + }); + + describe('cross-resource isolation', () => { + it('should independently track agent and promptGroup migrations', async () => { + const agent = await Agent.create({ + id: 'iso_agent', + name: 'Isolated Agent', + author: 'user1', + }); + await PromptGroup.create({ name: 'Isolated PG', author: 'user2' }); + + await AclEntry.create({ + principalType: 'user', + principalId: new Types.ObjectId(), + resourceType: 'agent', + resourceId: agent._id, + }); + + const migratedAgentIds = await AclEntry.distinct('resourceId', { + resourceType: 'agent', + principalType: 'user', + }); + const migratedPGIds = await AclEntry.distinct('resourceId', { + resourceType: 'promptGroup', + principalType: 'user', + }); + + const agentsToMigrate = await Agent.find({ + _id: { $nin: migratedAgentIds }, + author: { $exists: true, $ne: null }, + }).lean(); + + const pgsToMigrate = await PromptGroup.find({ + _id: { $nin: migratedPGIds }, + author: { $exists: true, $ne: null }, + }).lean(); + + expect(agentsToMigrate).toHaveLength(0); + expect(pgsToMigrate).toHaveLength(1); + }); + }); + + describe('scale behavior', () => { + it('should correctly handle many resources with partial migration', async () => { + const agents = []; + for (let i = 0; i < 20; i++) { + agents.push({ id: `agent_${i}`, name: `Agent ${i}`, author: `user_${i}` }); + } + const created = await Agent.insertMany(agents); + + const migrateEvens = created + .filter((_, i) => i % 2 === 0) + .map((a) => ({ + principalType: 'user', + principalId: new Types.ObjectId(), + resourceType: 'agent', + resourceId: a._id, + })); + await AclEntry.insertMany(migrateEvens); + + const migratedIds = await AclEntry.distinct('resourceId', { + resourceType: 'agent', + principalType: 'user', + }); + + const toMigrate = await Agent.find({ + _id: { $nin: migratedIds }, + author: { $exists: true, $ne: null }, + }).lean(); + + expect(toMigrate).toHaveLength(10); + const indices = toMigrate + .map((a) => parseInt(String(a.name).replace('Agent ', ''), 10)) + .sort((a, b) => a - b); + expect(indices).toEqual([1, 3, 5, 7, 9, 11, 13, 15, 17, 19]); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/multiTenancy.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/multiTenancy.ferretdb.spec.ts new file mode 100644 index 0000000000..a4d895f37a --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/multiTenancy.ferretdb.spec.ts @@ -0,0 +1,649 @@ +import mongoose from 'mongoose'; +import { execSync } from 'child_process'; +import { + actionSchema, + agentSchema, + agentApiKeySchema, + agentCategorySchema, + assistantSchema, + balanceSchema, + bannerSchema, + conversationTagSchema, + convoSchema, + fileSchema, + keySchema, + messageSchema, + pluginAuthSchema, + presetSchema, + projectSchema, + promptSchema, + promptGroupSchema, + roleSchema, + sessionSchema, + shareSchema, + tokenSchema, + toolCallSchema, + transactionSchema, + userSchema, + memorySchema, + groupSchema, +} from '~/schema'; +import accessRoleSchema from '~/schema/accessRole'; +import aclEntrySchema from '~/schema/aclEntry'; +import mcpServerSchema from '~/schema/mcpServer'; + +/** + * FerretDB Multi-Tenancy Benchmark + * + * Validates whether FerretDB can handle LibreChat's multi-tenancy model + * at scale using database-per-org isolation via Mongoose useDb(). + * + * Phases: + * 1. useDb schema mapping — verifies per-org PostgreSQL schema creation and data isolation + * 2. Index initialization — validates all 29 collections + 97 indexes, tests for deadlocks + * 3. Scaling curve — measures catalog growth, init time, and query latency at 10/50/100 orgs + * 4. Write amplification — compares update cost on high-index vs zero-index collections + * 5. Shared-collection alternative — benchmarks orgId-discriminated shared collections + * + * Run: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/mt_bench" \ + * npx jest multiTenancy.ferretdb --testTimeout=600000 + * + * Env vars: + * FERRETDB_URI — Required. FerretDB connection string. + * PG_CONTAINER — Docker container name for psql (default: librechat-ferretdb-postgres-1) + * SCALE_TIERS — Comma-separated org counts (default: 10,50,100) + * WRITE_AMP_DOCS — Number of docs for write amp test (default: 200) + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const PG_CONTAINER = process.env.PG_CONTAINER || 'librechat-ferretdb-postgres-1'; +const PG_USER = 'ferretdb'; +const ORG_PREFIX = 'mt_bench_'; + +const DEFAULT_TIERS = [10, 50, 100]; +const SCALE_TIERS: number[] = process.env.SCALE_TIERS + ? process.env.SCALE_TIERS.split(',').map(Number) + : DEFAULT_TIERS; + +const WRITE_AMP_DOCS = parseInt(process.env.WRITE_AMP_DOCS || '200', 10); + +/** All 29 LibreChat schemas by Mongoose model name */ +const MODEL_SCHEMAS: Record = { + User: userSchema, + Token: tokenSchema, + Session: sessionSchema, + Balance: balanceSchema, + Conversation: convoSchema, + Message: messageSchema, + Agent: agentSchema, + AgentApiKey: agentApiKeySchema, + AgentCategory: agentCategorySchema, + MCPServer: mcpServerSchema, + Role: roleSchema, + Action: actionSchema, + Assistant: assistantSchema, + File: fileSchema, + Banner: bannerSchema, + Project: projectSchema, + Key: keySchema, + PluginAuth: pluginAuthSchema, + Transaction: transactionSchema, + Preset: presetSchema, + Prompt: promptSchema, + PromptGroup: promptGroupSchema, + ConversationTag: conversationTagSchema, + SharedLink: shareSchema, + ToolCall: toolCallSchema, + MemoryEntry: memorySchema, + AccessRole: accessRoleSchema, + AclEntry: aclEntrySchema, + Group: groupSchema, +}; + +const MODEL_COUNT = Object.keys(MODEL_SCHEMAS).length; + +/** Register all 29 models on a given Mongoose Connection */ +function registerModels(conn: mongoose.Connection): Record> { + const models: Record> = {}; + for (const [name, schema] of Object.entries(MODEL_SCHEMAS)) { + models[name] = conn.models[name] || conn.model(name, schema); + } + return models; +} + +/** Initialize one org database: create all collections then build all indexes sequentially */ +async function initializeOrgDb(conn: mongoose.Connection): Promise<{ + models: Record>; + durationMs: number; +}> { + const models = registerModels(conn); + const start = Date.now(); + for (const model of Object.values(models)) { + await model.createCollection(); + await model.createIndexes(); + } + return { models, durationMs: Date.now() - start }; +} + +/** Execute a psql command against the FerretDB PostgreSQL backend via docker exec */ +function psql(query: string): string { + try { + const escaped = query.replace(/"/g, '\\"'); + return execSync( + `docker exec ${PG_CONTAINER} psql -U ${PG_USER} -d postgres -t -A -c "${escaped}"`, + { encoding: 'utf-8', timeout: 30_000 }, + ).trim(); + } catch { + return ''; + } +} + +/** + * Snapshot of DocumentDB catalog + PostgreSQL system catalog sizes. + * FerretDB with DocumentDB stores all data in a single `documentdb_data` schema. + * Each MongoDB collection → `documents_` + `retry_` table pair. + * The catalog lives in `documentdb_api_catalog.collections` and `.collection_indexes`. + */ +function catalogMetrics() { + return { + collections: parseInt(psql('SELECT count(*) FROM documentdb_api_catalog.collections'), 10) || 0, + databases: + parseInt( + psql('SELECT count(DISTINCT database_name) FROM documentdb_api_catalog.collections'), + 10, + ) || 0, + catalogIndexes: + parseInt(psql('SELECT count(*) FROM documentdb_api_catalog.collection_indexes'), 10) || 0, + dataTables: + parseInt( + psql( + "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'documentdb_data'", + ), + 10, + ) || 0, + pgClassTotal: parseInt(psql('SELECT count(*) FROM pg_class'), 10) || 0, + pgStatRows: parseInt(psql('SELECT count(*) FROM pg_statistic'), 10) || 0, + }; +} + +/** Measure point-query latency over N iterations and return percentile stats */ +async function measureLatency( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + model: mongoose.Model, + filter: Record, + iterations = 50, +) { + await model.findOne(filter).lean(); + + const times: number[] = []; + for (let i = 0; i < iterations; i++) { + const t0 = process.hrtime.bigint(); + await model.findOne(filter).lean(); + times.push(Number(process.hrtime.bigint() - t0) / 1e6); + } + + times.sort((a, b) => a - b); + return { + min: times[0], + max: times[times.length - 1], + median: times[Math.floor(times.length / 2)], + p95: times[Math.floor(times.length * 0.95)], + avg: times.reduce((s, v) => s + v, 0) / times.length, + }; +} + +function fmt(n: number): string { + return n.toFixed(2); +} + +describeIfFerretDB('FerretDB Multi-Tenancy Benchmark', () => { + const createdDbs: string[] = []; + + beforeAll(async () => { + await mongoose.connect(FERRETDB_URI as string, { autoIndex: false }); + }); + + afterAll(async () => { + for (const db of createdDbs) { + try { + await mongoose.connection.useDb(db, { useCache: false }).dropDatabase(); + } catch { + /* best-effort cleanup */ + } + } + try { + await mongoose.connection.dropDatabase(); + } catch { + /* best-effort */ + } + await mongoose.disconnect(); + }, 600_000); + + // ─── PHASE 1: DATABASE-PER-ORG SCHEMA MAPPING ──────────────────────────── + + describe('Phase 1: useDb Schema Mapping', () => { + const org1Db = `${ORG_PREFIX}iso_1`; + const org2Db = `${ORG_PREFIX}iso_2`; + let org1Models: Record>; + let org2Models: Record>; + + beforeAll(() => { + createdDbs.push(org1Db, org2Db); + }); + + it('creates separate databases with all 29 collections via useDb()', async () => { + const c1 = mongoose.connection.useDb(org1Db, { useCache: true }); + const c2 = mongoose.connection.useDb(org2Db, { useCache: true }); + + const r1 = await initializeOrgDb(c1); + const r2 = await initializeOrgDb(c2); + org1Models = r1.models; + org2Models = r2.models; + + console.log(`[Phase 1] org1 init: ${r1.durationMs}ms | org2 init: ${r2.durationMs}ms`); + + expect(Object.keys(org1Models)).toHaveLength(MODEL_COUNT); + expect(Object.keys(org2Models)).toHaveLength(MODEL_COUNT); + }, 120_000); + + it('maps each useDb database to a separate entry in the DocumentDB catalog', () => { + const raw = psql( + `SELECT database_name FROM documentdb_api_catalog.collections WHERE database_name LIKE '${ORG_PREFIX}%' GROUP BY database_name ORDER BY database_name`, + ); + const dbNames = raw.split('\n').filter(Boolean); + console.log('[Phase 1] DocumentDB databases:', dbNames); + + expect(dbNames).toContain(org1Db); + expect(dbNames).toContain(org2Db); + + const perDb = psql( + `SELECT database_name, count(*) FROM documentdb_api_catalog.collections WHERE database_name LIKE '${ORG_PREFIX}%' GROUP BY database_name ORDER BY database_name`, + ); + console.log('[Phase 1] Collections per database:\n' + perDb); + }); + + it('isolates data between org databases', async () => { + await org1Models.User.create({ + name: 'Org1 User', + email: 'u@org1.test', + username: 'org1user', + }); + await org2Models.User.create({ + name: 'Org2 User', + email: 'u@org2.test', + username: 'org2user', + }); + + const u1 = await org1Models.User.find({}).lean(); + const u2 = await org2Models.User.find({}).lean(); + + expect(u1).toHaveLength(1); + expect(u2).toHaveLength(1); + expect((u1[0] as Record).email).toBe('u@org1.test'); + expect((u2[0] as Record).email).toBe('u@org2.test'); + }, 30_000); + }); + + // ─── PHASE 2: INDEX INITIALIZATION ──────────────────────────────────────── + + describe('Phase 2: Index Initialization', () => { + const seqDb = `${ORG_PREFIX}idx_seq`; + + beforeAll(() => { + createdDbs.push(seqDb); + }); + + it('creates all indexes sequentially and reports per-model breakdown', async () => { + const conn = mongoose.connection.useDb(seqDb, { useCache: true }); + const models = registerModels(conn); + + const stats: { name: string; ms: number; idxCount: number }[] = []; + for (const [name, model] of Object.entries(models)) { + const t0 = Date.now(); + await model.createCollection(); + await model.createIndexes(); + const idxs = await model.collection.indexes(); + stats.push({ name, ms: Date.now() - t0, idxCount: idxs.length - 1 }); + } + + const totalMs = stats.reduce((s, r) => s + r.ms, 0); + const totalIdx = stats.reduce((s, r) => s + r.idxCount, 0); + + console.log(`[Phase 2] Sequential: ${totalMs}ms total, ${totalIdx} custom indexes`); + console.log('[Phase 2] Slowest 10:'); + for (const s of stats.sort((a, b) => b.ms - a.ms).slice(0, 10)) { + console.log(` ${s.name.padEnd(20)} ${String(s.idxCount).padStart(3)} indexes ${s.ms}ms`); + } + + expect(totalIdx).toBeGreaterThanOrEqual(90); + }, 120_000); + + it('tests concurrent index creation for deadlock risk', async () => { + const concDb = `${ORG_PREFIX}idx_conc`; + createdDbs.push(concDb); + const conn = mongoose.connection.useDb(concDb, { useCache: false }); + const models = registerModels(conn); + + for (const model of Object.values(models)) { + await model.createCollection(); + } + + const t0 = Date.now(); + try { + await Promise.all(Object.values(models).map((m) => m.createIndexes())); + console.log(`[Phase 2] Concurrent: ${Date.now() - t0}ms — no deadlock`); + } catch (err) { + console.warn( + `[Phase 2] Concurrent: DEADLOCKED after ${Date.now() - t0}ms — ${(err as Error).message}`, + ); + } + }, 120_000); + + it('verifies sparse, partial, and TTL index types on FerretDB', async () => { + const conn = mongoose.connection.useDb(seqDb, { useCache: true }); + + const userIdxs = await conn.model('User').collection.indexes(); + const sparseCount = userIdxs.filter((i: Record) => i.sparse).length; + const ttlCount = userIdxs.filter( + (i: Record) => i.expireAfterSeconds !== undefined, + ).length; + console.log( + `[Phase 2] User: ${userIdxs.length} total, ${sparseCount} sparse, ${ttlCount} TTL`, + ); + expect(sparseCount).toBeGreaterThanOrEqual(8); + + const fileIdxs = await conn.model('File').collection.indexes(); + const partialFile = fileIdxs.find( + (i: Record) => i.partialFilterExpression != null, + ); + console.log(`[Phase 2] File partialFilterExpression: ${partialFile ? 'YES' : 'NO'}`); + expect(partialFile).toBeDefined(); + + const groupIdxs = await conn.model('Group').collection.indexes(); + const sparseGroup = groupIdxs.find((i: Record) => i.sparse); + const partialGroup = groupIdxs.find( + (i: Record) => i.partialFilterExpression != null, + ); + console.log( + `[Phase 2] Group: sparse=${sparseGroup ? 'YES' : 'NO'}, partial=${partialGroup ? 'YES' : 'NO'}`, + ); + expect(sparseGroup).toBeDefined(); + expect(partialGroup).toBeDefined(); + }, 60_000); + }); + + // ─── PHASE 3: SCALING CURVE ─────────────────────────────────────────────── + + describe('Phase 3: Scaling Curve', () => { + interface TierResult { + tier: number; + batchMs: number; + avgPerOrg: number; + catalog: ReturnType; + latency: Awaited>; + } + + const tierResults: TierResult[] = []; + let orgsCreated = 0; + let firstOrgConn: mongoose.Connection | null = null; + + beforeAll(() => { + const baseline = catalogMetrics(); + console.log( + `[Phase 3] Baseline — collections: ${baseline.collections}, ` + + `databases: ${baseline.databases}, catalog indexes: ${baseline.catalogIndexes}, ` + + `data tables: ${baseline.dataTables}, pg_class: ${baseline.pgClassTotal}`, + ); + }); + + it.each(SCALE_TIERS)( + 'scales to %i orgs', + async (target) => { + const t0 = Date.now(); + + for (let i = orgsCreated + 1; i <= target; i++) { + const dbName = `${ORG_PREFIX}s${i}`; + createdDbs.push(dbName); + + const conn = mongoose.connection.useDb(dbName, { useCache: i === 1 }); + if (i === 1) { + firstOrgConn = conn; + } + + const models = registerModels(conn); + for (const model of Object.values(models)) { + await model.createCollection(); + await model.createIndexes(); + } + + if (i === 1) { + await models.User.create({ + name: 'Latency Probe', + email: 'probe@scale.test', + username: 'probe', + }); + } + + if (i % 10 === 0) { + process.stdout.write(` ${i}/${target} orgs\n`); + } + } + + const batchMs = Date.now() - t0; + const batchSize = target - orgsCreated; + orgsCreated = target; + + const lat = await measureLatency(firstOrgConn!.model('User'), { + email: 'probe@scale.test', + }); + const cat = catalogMetrics(); + + tierResults.push({ + tier: target, + batchMs, + avgPerOrg: batchSize > 0 ? Math.round(batchMs / batchSize) : 0, + catalog: cat, + latency: lat, + }); + + console.log(`\n[Phase 3] === ${target} orgs ===`); + console.log( + ` Init: ${batchMs}ms total (${batchSize > 0 ? Math.round(batchMs / batchSize) : 0}ms/org, batch=${batchSize})`, + ); + console.log( + ` Query: avg=${fmt(lat.avg)}ms median=${fmt(lat.median)}ms p95=${fmt(lat.p95)}ms`, + ); + console.log( + ` Catalog: ${cat.collections} collections, ${cat.catalogIndexes} indexes, ` + + `${cat.dataTables} data tables, pg_class=${cat.pgClassTotal}`, + ); + + expect(cat.collections).toBeGreaterThan(0); + }, + 600_000, + ); + + afterAll(() => { + if (tierResults.length === 0) { + return; + } + + const hdr = [ + 'Orgs', + 'Colls', + 'CatIdx', + 'DataTbls', + 'pg_class', + 'Init/org', + 'Qry avg', + 'Qry p95', + ]; + const w = [8, 10, 10, 10, 12, 12, 12, 12]; + + console.log('\n[Phase 3] SCALING SUMMARY'); + console.log('─'.repeat(w.reduce((a, b) => a + b))); + console.log(hdr.map((h, i) => h.padEnd(w[i])).join('')); + console.log('─'.repeat(w.reduce((a, b) => a + b))); + + for (const r of tierResults) { + const row = [ + String(r.tier), + String(r.catalog.collections), + String(r.catalog.catalogIndexes), + String(r.catalog.dataTables), + String(r.catalog.pgClassTotal), + `${r.avgPerOrg}ms`, + `${fmt(r.latency.avg)}ms`, + `${fmt(r.latency.p95)}ms`, + ]; + console.log(row.map((v, i) => v.padEnd(w[i])).join('')); + } + console.log('─'.repeat(w.reduce((a, b) => a + b))); + }); + }); + + // ─── PHASE 4: WRITE AMPLIFICATION ──────────────────────────────────────── + + describe('Phase 4: Write Amplification', () => { + it('compares update cost: high-index (User, 11+ idx) vs zero-index collection', async () => { + const db = `${ORG_PREFIX}wamp`; + createdDbs.push(db); + const conn = mongoose.connection.useDb(db, { useCache: false }); + + const HighIdx = conn.model('User', userSchema); + await HighIdx.createCollection(); + await HighIdx.createIndexes(); + + const bareSchema = new mongoose.Schema({ name: String, email: String, ts: Date }); + const LowIdx = conn.model('BareDoc', bareSchema); + await LowIdx.createCollection(); + + const N = WRITE_AMP_DOCS; + + await HighIdx.insertMany( + Array.from({ length: N }, (_, i) => ({ + name: `U${i}`, + email: `u${i}@wamp.test`, + username: `u${i}`, + })), + ); + await LowIdx.insertMany( + Array.from({ length: N }, (_, i) => ({ + name: `U${i}`, + email: `u${i}@wamp.test`, + ts: new Date(), + })), + ); + + const walBefore = psql('SELECT wal_bytes FROM pg_stat_wal'); + + const highStart = Date.now(); + for (let i = 0; i < N; i++) { + await HighIdx.updateOne({ email: `u${i}@wamp.test` }, { $set: { name: `X${i}` } }); + } + const highMs = Date.now() - highStart; + + const walMid = psql('SELECT wal_bytes FROM pg_stat_wal'); + + const lowStart = Date.now(); + for (let i = 0; i < N; i++) { + await LowIdx.updateOne({ email: `u${i}@wamp.test` }, { $set: { name: `X${i}` } }); + } + const lowMs = Date.now() - lowStart; + + const walAfter = psql('SELECT wal_bytes FROM pg_stat_wal'); + + console.log(`\n[Phase 4] Write Amplification (${N} updates each)`); + console.log(` High-index (User, 11+ idx): ${highMs}ms (${fmt(highMs / N)}ms/op)`); + console.log(` Zero-index (bare): ${lowMs}ms (${fmt(lowMs / N)}ms/op)`); + console.log(` Time ratio: ${fmt(highMs / Math.max(lowMs, 1))}x`); + + if (walBefore && walMid && walAfter) { + const wHigh = BigInt(walMid) - BigInt(walBefore); + const wLow = BigInt(walAfter) - BigInt(walMid); + console.log(` WAL: high-idx=${wHigh} bytes, bare=${wLow} bytes`); + if (wLow > BigInt(0)) { + console.log(` WAL ratio: ${fmt(Number(wHigh) / Number(wLow))}x`); + } + } + + expect(highMs).toBeGreaterThan(0); + expect(lowMs).toBeGreaterThan(0); + }, 300_000); + }); + + // ─── PHASE 5: SHARED-COLLECTION ALTERNATIVE ────────────────────────────── + + describe('Phase 5: Shared Collection Alternative', () => { + it('benchmarks shared collection with orgId discriminator field', async () => { + const db = `${ORG_PREFIX}shared`; + createdDbs.push(db); + const conn = mongoose.connection.useDb(db, { useCache: false }); + + const sharedSchema = new mongoose.Schema({ + orgId: { type: String, required: true, index: true }, + name: String, + email: String, + username: String, + provider: { type: String, default: 'local' }, + role: { type: String, default: 'USER' }, + }); + sharedSchema.index({ orgId: 1, email: 1 }, { unique: true }); + + const Shared = conn.model('SharedUser', sharedSchema); + await Shared.createCollection(); + await Shared.createIndexes(); + + const ORG_N = 100; + const USERS_PER = 50; + + const docs = []; + for (let o = 0; o < ORG_N; o++) { + for (let u = 0; u < USERS_PER; u++) { + docs.push({ + orgId: `org_${o}`, + name: `User ${u}`, + email: `u${u}@o${o}.test`, + username: `u${u}_o${o}`, + }); + } + } + + const insertT0 = Date.now(); + await Shared.insertMany(docs, { ordered: false }); + const insertMs = Date.now() - insertT0; + + const totalDocs = ORG_N * USERS_PER; + console.log(`\n[Phase 5] Shared collection: ${totalDocs} docs inserted in ${insertMs}ms`); + + const pointLat = await measureLatency(Shared, { + orgId: 'org_50', + email: 'u25@o50.test', + }); + console.log( + ` Point query: avg=${fmt(pointLat.avg)}ms median=${fmt(pointLat.median)}ms p95=${fmt(pointLat.p95)}ms`, + ); + + const listT0 = Date.now(); + const orgDocs = await Shared.find({ orgId: 'org_50' }).lean(); + const listMs = Date.now() - listT0; + console.log(` List org users (${orgDocs.length} docs): ${listMs}ms`); + + const countT0 = Date.now(); + const count = await Shared.countDocuments({ orgId: 'org_50' }); + const countMs = Date.now() - countT0; + console.log(` Count org users: ${count} in ${countMs}ms`); + + const cat = catalogMetrics(); + console.log( + ` Catalog: ${cat.collections} collections, ${cat.catalogIndexes} indexes, ` + + `${cat.dataTables} data tables (shared approach = 1 extra db, minimal overhead)`, + ); + + expect(orgDocs).toHaveLength(USERS_PER); + }, 120_000); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/orgOperations.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/orgOperations.ferretdb.spec.ts new file mode 100644 index 0000000000..fdea2eb8fc --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/orgOperations.ferretdb.spec.ts @@ -0,0 +1,675 @@ +import mongoose, { Schema, type Connection, type Model } from 'mongoose'; +import { + actionSchema, + agentSchema, + agentApiKeySchema, + agentCategorySchema, + assistantSchema, + balanceSchema, + bannerSchema, + conversationTagSchema, + convoSchema, + fileSchema, + keySchema, + messageSchema, + pluginAuthSchema, + presetSchema, + projectSchema, + promptSchema, + promptGroupSchema, + roleSchema, + sessionSchema, + shareSchema, + tokenSchema, + toolCallSchema, + transactionSchema, + userSchema, + memorySchema, + groupSchema, +} from '~/schema'; +import accessRoleSchema from '~/schema/accessRole'; +import mcpServerSchema from '~/schema/mcpServer'; +import aclEntrySchema from '~/schema/aclEntry'; +import { initializeOrgCollections, createIndexesWithRetry, retryWithBackoff } from '~/utils/retry'; + +/** + * Production operations tests for FerretDB multi-tenancy: + * 1. Retry utility under simulated and real deadlock conditions + * 2. Programmatic per-org backup/restore (driver-level, no mongodump) + * 3. Schema migration across existing org databases + * + * Run: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/ops_test" \ + * npx jest orgOperations.ferretdb --testTimeout=300000 + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const DB_PREFIX = 'ops_test_'; + +const MODEL_SCHEMAS: Record = { + User: userSchema, + Token: tokenSchema, + Session: sessionSchema, + Balance: balanceSchema, + Conversation: convoSchema, + Message: messageSchema, + Agent: agentSchema, + AgentApiKey: agentApiKeySchema, + AgentCategory: agentCategorySchema, + MCPServer: mcpServerSchema, + Role: roleSchema, + Action: actionSchema, + Assistant: assistantSchema, + File: fileSchema, + Banner: bannerSchema, + Project: projectSchema, + Key: keySchema, + PluginAuth: pluginAuthSchema, + Transaction: transactionSchema, + Preset: presetSchema, + Prompt: promptSchema, + PromptGroup: promptGroupSchema, + ConversationTag: conversationTagSchema, + SharedLink: shareSchema, + ToolCall: toolCallSchema, + MemoryEntry: memorySchema, + AccessRole: accessRoleSchema, + AclEntry: aclEntrySchema, + Group: groupSchema, +}; + +const MODEL_COUNT = Object.keys(MODEL_SCHEMAS).length; + +function registerModels(conn: Connection): Record> { + const models: Record> = {}; + for (const [name, schema] of Object.entries(MODEL_SCHEMAS)) { + models[name] = conn.models[name] || conn.model(name, schema); + } + return models; +} + +// ─── BACKUP/RESTORE UTILITIES ─────────────────────────────────────────────── + +interface OrgBackup { + orgId: string; + timestamp: Date; + collections: Record; +} + +/** Dump all collections from an org database to an in-memory structure */ +async function backupOrg(conn: Connection, orgId: string): Promise { + const collectionNames = (await conn.db!.listCollections().toArray()).map((c) => c.name); + const collections: Record = {}; + + for (const name of collectionNames) { + if (name.startsWith('system.')) { + continue; + } + const docs = await conn.db!.collection(name).find({}).toArray(); + collections[name] = docs; + } + + return { orgId, timestamp: new Date(), collections }; +} + +/** Restore collections from a backup into a target connection */ +async function restoreOrg( + conn: Connection, + backup: OrgBackup, +): Promise<{ collectionsRestored: number; docsRestored: number }> { + let docsRestored = 0; + + for (const [name, docs] of Object.entries(backup.collections)) { + if (docs.length === 0) { + continue; + } + const collection = conn.db!.collection(name); + await collection.insertMany(docs as Array>); + docsRestored += docs.length; + } + + return { collectionsRestored: Object.keys(backup.collections).length, docsRestored }; +} + +// ─── MIGRATION UTILITIES ──────────────────────────────────────────────────── + +interface MigrationResult { + orgId: string; + newCollections: string[]; + indexResults: Array<{ model: string; created: boolean; ms: number }>; + totalMs: number; +} + +/** Migrate a single org: ensure all collections exist and all indexes are current */ +async function migrateOrg( + conn: Connection, + orgId: string, + schemas: Record, +): Promise { + const t0 = Date.now(); + const models = registerModels(conn); + const existingCollections = new Set( + (await conn.db!.listCollections().toArray()).map((c) => c.name), + ); + + const newCollections: string[] = []; + const indexResults: Array<{ model: string; created: boolean; ms: number }> = []; + + for (const [name, model] of Object.entries(models)) { + const collName = model.collection.collectionName; + const isNew = !existingCollections.has(collName); + if (isNew) { + newCollections.push(name); + } + + const mt0 = Date.now(); + await model.createCollection(); + await createIndexesWithRetry(model); + indexResults.push({ model: name, created: isNew, ms: Date.now() - mt0 }); + } + + return { orgId, newCollections, indexResults, totalMs: Date.now() - t0 }; +} + +/** Migrate all orgs in sequence with progress reporting */ +async function migrateAllOrgs( + baseConn: Connection, + orgIds: string[], + schemas: Record, + onProgress?: (completed: number, total: number, result: MigrationResult) => void, +): Promise { + const results: MigrationResult[] = []; + + for (let i = 0; i < orgIds.length; i++) { + const orgId = orgIds[i]; + const conn = baseConn.useDb(`${DB_PREFIX}org_${orgId}`, { useCache: true }); + const result = await migrateOrg(conn, orgId, schemas); + results.push(result); + if (onProgress) { + onProgress(i + 1, orgIds.length, result); + } + } + + return results; +} + +// ─── TESTS ────────────────────────────────────────────────────────────────── + +describeIfFerretDB('Org Operations (Production)', () => { + const createdDbs: string[] = []; + let baseConn: Connection; + + beforeAll(async () => { + baseConn = await mongoose.createConnection(FERRETDB_URI as string).asPromise(); + }); + + afterAll(async () => { + for (const db of createdDbs) { + try { + await baseConn.useDb(db, { useCache: false }).dropDatabase(); + } catch { + /* best-effort */ + } + } + await baseConn.close(); + }, 120_000); + + // ─── RETRY UTILITY ────────────────────────────────────────────────────── + + describe('retryWithBackoff', () => { + it('succeeds on first attempt when no error', async () => { + let calls = 0; + const result = await retryWithBackoff(async () => { + calls++; + return 'ok'; + }, 'test-op'); + expect(result).toBe('ok'); + expect(calls).toBe(1); + }); + + it('retries on deadlock error and eventually succeeds', async () => { + let calls = 0; + const result = await retryWithBackoff( + async () => { + calls++; + if (calls < 3) { + throw new Error('deadlock detected'); + } + return 'recovered'; + }, + 'deadlock-test', + { baseDelayMs: 10, jitter: false }, + ); + + expect(result).toBe('recovered'); + expect(calls).toBe(3); + }); + + it('does not retry on non-retryable errors', async () => { + let calls = 0; + await expect( + retryWithBackoff( + async () => { + calls++; + throw new Error('validation failed'); + }, + 'non-retryable', + { baseDelayMs: 10 }, + ), + ).rejects.toThrow('validation failed'); + expect(calls).toBe(1); + }); + + it('exhausts max attempts and throws', async () => { + let calls = 0; + await expect( + retryWithBackoff( + async () => { + calls++; + throw new Error('deadlock detected'); + }, + 'exhausted', + { maxAttempts: 3, baseDelayMs: 10, jitter: false }, + ), + ).rejects.toThrow('deadlock'); + expect(calls).toBe(3); + }); + + it('respects maxDelayMs cap', async () => { + const delays: number[] = []; + let calls = 0; + + await retryWithBackoff( + async () => { + calls++; + if (calls < 4) { + throw new Error('deadlock detected'); + } + return 'ok'; + }, + 'delay-cap', + { + baseDelayMs: 100, + maxDelayMs: 250, + jitter: false, + onRetry: (_err, _attempt, delay) => delays.push(delay), + }, + ); + + expect(delays[0]).toBe(100); + expect(delays[1]).toBe(200); + expect(delays[2]).toBe(250); + }); + }); + + // ─── REAL DEADLOCK RETRY ──────────────────────────────────────────────── + + describe('initializeOrgCollections with retry', () => { + it('provisions 5 orgs sequentially using the production utility', async () => { + const orgIds = ['retry_1', 'retry_2', 'retry_3', 'retry_4', 'retry_5']; + const results: Array<{ orgId: string; ms: number; models: number }> = []; + + for (const orgId of orgIds) { + const dbName = `${DB_PREFIX}org_${orgId}`; + createdDbs.push(dbName); + const conn = baseConn.useDb(dbName, { useCache: true }); + const models = registerModels(conn); + + const { totalMs } = await initializeOrgCollections(models, { + baseDelayMs: 50, + maxAttempts: 5, + }); + results.push({ orgId, ms: totalMs, models: Object.keys(models).length }); + } + + const totalMs = results.reduce((s, r) => s + r.ms, 0); + console.log(`[Retry] 5 orgs provisioned in ${totalMs}ms:`); + for (const r of results) { + console.log(` ${r.orgId}: ${r.ms}ms (${r.models} models)`); + } + + expect(results.every((r) => r.models === MODEL_COUNT)).toBe(true); + }, 120_000); + }); + + // ─── BACKUP/RESTORE ───────────────────────────────────────────────────── + + describe('per-org backup and restore', () => { + const sourceOrg = 'backup_src'; + const targetOrg = 'backup_dst'; + + beforeAll(async () => { + const srcDb = `${DB_PREFIX}org_${sourceOrg}`; + createdDbs.push(srcDb, `${DB_PREFIX}org_${targetOrg}`); + const srcConn = baseConn.useDb(srcDb, { useCache: true }); + const models = registerModels(srcConn); + await initializeOrgCollections(models); + + await models.User.create([ + { name: 'Alice', email: 'alice@backup.test', username: 'alice' }, + { name: 'Bob', email: 'bob@backup.test', username: 'bob' }, + { name: 'Charlie', email: 'charlie@backup.test', username: 'charlie' }, + ]); + + await models.Conversation.create([ + { + conversationId: 'conv_1', + user: 'alice_id', + title: 'Test conversation 1', + endpoint: 'openAI', + model: 'gpt-4', + }, + { + conversationId: 'conv_2', + user: 'bob_id', + title: 'Test conversation 2', + endpoint: 'openAI', + model: 'gpt-4', + }, + ]); + + await models.Message.create([ + { + messageId: 'msg_1', + conversationId: 'conv_1', + user: 'alice_id', + sender: 'user', + text: 'Hello world', + isCreatedByUser: true, + }, + { + messageId: 'msg_2', + conversationId: 'conv_1', + user: 'alice_id', + sender: 'GPT-4', + text: 'Hi there!', + isCreatedByUser: false, + }, + ]); + + const agentId = new mongoose.Types.ObjectId(); + await models.Agent.create({ + id: `agent_${agentId}`, + name: 'Test Agent', + author: new mongoose.Types.ObjectId(), + description: 'A test agent for backup', + provider: 'openAI', + model: 'gpt-4', + }); + }, 60_000); + + it('backs up all collections from the source org', async () => { + const srcConn = baseConn.useDb(`${DB_PREFIX}org_${sourceOrg}`, { useCache: true }); + const backup = await backupOrg(srcConn, sourceOrg); + + console.log(`[Backup] ${sourceOrg}:`); + console.log(` Timestamp: ${backup.timestamp.toISOString()}`); + console.log(` Collections: ${Object.keys(backup.collections).length}`); + let totalDocs = 0; + for (const [name, docs] of Object.entries(backup.collections)) { + if (docs.length > 0) { + console.log(` ${name}: ${docs.length} docs`); + totalDocs += docs.length; + } + } + console.log(` Total documents: ${totalDocs}`); + + expect(Object.keys(backup.collections).length).toBeGreaterThanOrEqual(4); + expect(backup.collections['users']?.length).toBe(3); + expect(backup.collections['conversations']?.length).toBe(2); + expect(backup.collections['messages']?.length).toBe(2); + }, 30_000); + + it('restores backup to a fresh org database', async () => { + const srcConn = baseConn.useDb(`${DB_PREFIX}org_${sourceOrg}`, { useCache: true }); + const backup = await backupOrg(srcConn, sourceOrg); + + const dstConn = baseConn.useDb(`${DB_PREFIX}org_${targetOrg}`, { useCache: true }); + const dstModels = registerModels(dstConn); + await initializeOrgCollections(dstModels); + + const { collectionsRestored, docsRestored } = await restoreOrg(dstConn, backup); + + console.log( + `[Restore] ${targetOrg}: ${collectionsRestored} collections, ${docsRestored} docs`, + ); + + expect(docsRestored).toBeGreaterThanOrEqual(7); + }, 60_000); + + it('verifies restored data matches source exactly', async () => { + const srcConn = baseConn.useDb(`${DB_PREFIX}org_${sourceOrg}`, { useCache: true }); + const dstConn = baseConn.useDb(`${DB_PREFIX}org_${targetOrg}`, { useCache: true }); + + const srcUsers = await srcConn.db!.collection('users').find({}).sort({ email: 1 }).toArray(); + const dstUsers = await dstConn.db!.collection('users').find({}).sort({ email: 1 }).toArray(); + + expect(dstUsers.length).toBe(srcUsers.length); + for (let i = 0; i < srcUsers.length; i++) { + expect(dstUsers[i].name).toBe(srcUsers[i].name); + expect(dstUsers[i].email).toBe(srcUsers[i].email); + expect(dstUsers[i]._id.toString()).toBe(srcUsers[i]._id.toString()); + } + + const srcMsgs = await srcConn + .db!.collection('messages') + .find({}) + .sort({ messageId: 1 }) + .toArray(); + const dstMsgs = await dstConn + .db!.collection('messages') + .find({}) + .sort({ messageId: 1 }) + .toArray(); + + expect(dstMsgs.length).toBe(srcMsgs.length); + for (let i = 0; i < srcMsgs.length; i++) { + expect(dstMsgs[i].messageId).toBe(srcMsgs[i].messageId); + expect(dstMsgs[i].text).toBe(srcMsgs[i].text); + expect(dstMsgs[i]._id.toString()).toBe(srcMsgs[i]._id.toString()); + } + + const srcConvos = await srcConn + .db!.collection('conversations') + .find({}) + .sort({ conversationId: 1 }) + .toArray(); + const dstConvos = await dstConn + .db!.collection('conversations') + .find({}) + .sort({ conversationId: 1 }) + .toArray(); + + expect(dstConvos.length).toBe(srcConvos.length); + for (let i = 0; i < srcConvos.length; i++) { + expect(dstConvos[i].conversationId).toBe(srcConvos[i].conversationId); + expect(dstConvos[i].title).toBe(srcConvos[i].title); + } + + console.log('[Restore] Data integrity verified: _ids, fields, and counts match exactly'); + }, 30_000); + + it('verifies BSON type preservation (ObjectId, Date, Number)', async () => { + const dstConn = baseConn.useDb(`${DB_PREFIX}org_${targetOrg}`, { useCache: true }); + + const user = await dstConn.db!.collection('users').findOne({ email: 'alice@backup.test' }); + expect(user).toBeDefined(); + expect(user!._id).toBeInstanceOf(mongoose.Types.ObjectId); + expect(user!.createdAt).toBeInstanceOf(Date); + + const agent = await dstConn.db!.collection('agents').findOne({}); + expect(agent).toBeDefined(); + expect(agent!._id).toBeInstanceOf(mongoose.Types.ObjectId); + expect(typeof agent!.name).toBe('string'); + + console.log('[Restore] BSON types preserved: ObjectId, Date, String all correct'); + }); + + it('measures backup and restore performance', async () => { + const srcConn = baseConn.useDb(`${DB_PREFIX}org_${sourceOrg}`, { useCache: true }); + + const backupStart = Date.now(); + const backup = await backupOrg(srcConn, sourceOrg); + const backupMs = Date.now() - backupStart; + + const freshDb = `${DB_PREFIX}org_perf_restore`; + createdDbs.push(freshDb); + const freshConn = baseConn.useDb(freshDb, { useCache: false }); + const freshModels = registerModels(freshConn); + await initializeOrgCollections(freshModels); + + const restoreStart = Date.now(); + await restoreOrg(freshConn, backup); + const restoreMs = Date.now() - restoreStart; + + const totalDocs = Object.values(backup.collections).reduce((s, d) => s + d.length, 0); + console.log( + `[Perf] Backup: ${backupMs}ms (${totalDocs} docs across ${Object.keys(backup.collections).length} collections)`, + ); + console.log(`[Perf] Restore: ${restoreMs}ms`); + + expect(backupMs).toBeLessThan(5000); + expect(restoreMs).toBeLessThan(5000); + }, 60_000); + }); + + // ─── SCHEMA MIGRATION ────────────────────────────────────────────────── + + describe('schema migration across orgs', () => { + const migrationOrgs = ['mig_1', 'mig_2', 'mig_3', 'mig_4', 'mig_5']; + + beforeAll(async () => { + for (const orgId of migrationOrgs) { + const dbName = `${DB_PREFIX}org_${orgId}`; + createdDbs.push(dbName); + const conn = baseConn.useDb(dbName, { useCache: true }); + const models = registerModels(conn); + await initializeOrgCollections(models); + + await models.User.create({ + name: `User ${orgId}`, + email: `user@${orgId}.test`, + username: orgId, + }); + } + }, 120_000); + + it('createIndexes is idempotent (no-op for existing indexes)', async () => { + const conn = baseConn.useDb(`${DB_PREFIX}org_mig_1`, { useCache: true }); + const models = registerModels(conn); + + const beforeIndexes = await models.User.collection.indexes(); + + const t0 = Date.now(); + await initializeOrgCollections(models); + const ms = Date.now() - t0; + + const afterIndexes = await models.User.collection.indexes(); + + expect(afterIndexes.length).toBe(beforeIndexes.length); + console.log( + `[Migration] Idempotent re-init: ${ms}ms (indexes unchanged: ${beforeIndexes.length})`, + ); + }, 60_000); + + it('adds a new collection to all existing orgs', async () => { + const newSchema = new Schema( + { + orgId: { type: String, index: true }, + eventType: { type: String, required: true, index: true }, + payload: Schema.Types.Mixed, + userId: { type: Schema.Types.ObjectId, index: true }, + }, + { timestamps: true }, + ); + newSchema.index({ orgId: 1, eventType: 1, createdAt: -1 }); + + for (const orgId of migrationOrgs) { + const conn = baseConn.useDb(`${DB_PREFIX}org_${orgId}`, { useCache: true }); + const AuditLog = conn.models['AuditLog'] || conn.model('AuditLog', newSchema); + await AuditLog.createCollection(); + await createIndexesWithRetry(AuditLog); + } + + for (const orgId of migrationOrgs) { + const conn = baseConn.useDb(`${DB_PREFIX}org_${orgId}`, { useCache: true }); + const collections = (await conn.db!.listCollections().toArray()).map((c) => c.name); + expect(collections).toContain('auditlogs'); + + const indexes = await conn.db!.collection('auditlogs').indexes(); + expect(indexes.length).toBeGreaterThanOrEqual(4); + } + + console.log( + `[Migration] New collection 'auditlogs' added to ${migrationOrgs.length} orgs with 4+ indexes`, + ); + }, 60_000); + + it('adds a new index to an existing collection across all orgs', async () => { + const indexSpec = { username: 1, createdAt: -1 }; + + for (const orgId of migrationOrgs) { + const conn = baseConn.useDb(`${DB_PREFIX}org_${orgId}`, { useCache: true }); + await retryWithBackoff( + () => conn.db!.collection('users').createIndex(indexSpec, { background: true }), + `createIndex(users, username+createdAt) for ${orgId}`, + ); + } + + for (const orgId of migrationOrgs) { + const conn = baseConn.useDb(`${DB_PREFIX}org_${orgId}`, { useCache: true }); + const indexes = await conn.db!.collection('users').indexes(); + const hasNewIdx = indexes.some( + (idx: Record) => JSON.stringify(idx.key) === JSON.stringify(indexSpec), + ); + expect(hasNewIdx).toBe(true); + } + + console.log( + `[Migration] New compound index added to 'users' across ${migrationOrgs.length} orgs`, + ); + }, 60_000); + + it('runs migrateAllOrgs and reports progress', async () => { + const progress: string[] = []; + + const results = await migrateAllOrgs( + baseConn, + migrationOrgs, + MODEL_SCHEMAS, + (completed, total, result) => { + progress.push( + `${completed}/${total}: ${result.orgId} — ${result.totalMs}ms, ${result.newCollections.length} new collections`, + ); + }, + ); + + console.log(`[Migration] Full migration across ${migrationOrgs.length} orgs:`); + for (const p of progress) { + console.log(` ${p}`); + } + + const totalMs = results.reduce((s, r) => s + r.totalMs, 0); + const avgMs = Math.round(totalMs / results.length); + console.log(` Total: ${totalMs}ms, avg: ${avgMs}ms/org`); + + expect(results).toHaveLength(migrationOrgs.length); + expect(results.every((r) => r.indexResults.length >= MODEL_COUNT)).toBe(true); + }, 120_000); + + it('verifies existing data is preserved after migration', async () => { + for (const orgId of migrationOrgs) { + const conn = baseConn.useDb(`${DB_PREFIX}org_${orgId}`, { useCache: true }); + const user = await conn.db!.collection('users').findOne({ email: `user@${orgId}.test` }); + expect(user).toBeDefined(); + expect(user!.name).toBe(`User ${orgId}`); + } + + console.log( + `[Migration] All existing user data preserved across ${migrationOrgs.length} orgs`, + ); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts new file mode 100644 index 0000000000..7e6c8ad1b0 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts @@ -0,0 +1,353 @@ +import mongoose, { Schema, Types } from 'mongoose'; + +/** + * Integration tests for the Prompt $lookup → find + attach replacement. + * + * These verify that prompt group listing with production prompt + * resolution works identically on both MongoDB and FerretDB + * using only standard find/countDocuments (no $lookup). + * + * Run against FerretDB: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/prompt_lookup_test" npx jest promptLookup.ferretdb + * + * Run against MongoDB (for parity): + * FERRETDB_URI="mongodb://127.0.0.1:27017/prompt_lookup_test" npx jest promptLookup.ferretdb + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const promptGroupSchema = new Schema( + { + name: { type: String, required: true, index: true }, + numberOfGenerations: { type: Number, default: 0 }, + oneliner: { type: String, default: '' }, + category: { type: String, default: '', index: true }, + productionId: { type: Schema.Types.ObjectId, ref: 'FDBPrompt', index: true }, + author: { type: Schema.Types.ObjectId, required: true, index: true }, + authorName: { type: String, required: true }, + command: { type: String }, + projectIds: { type: [Schema.Types.ObjectId], default: [] }, + }, + { timestamps: true }, +); + +const promptSchema = new Schema( + { + groupId: { type: Schema.Types.ObjectId, ref: 'FDBPromptGroup', required: true }, + author: { type: Schema.Types.ObjectId, required: true }, + prompt: { type: String, required: true }, + type: { type: String, enum: ['text', 'chat'], required: true }, + }, + { timestamps: true }, +); + +type PromptGroupDoc = mongoose.Document & { + name: string; + productionId: Types.ObjectId; + author: Types.ObjectId; + authorName: string; + category: string; + oneliner: string; + numberOfGenerations: number; + command?: string; + projectIds: Types.ObjectId[]; + createdAt: Date; + updatedAt: Date; +}; + +type PromptDoc = mongoose.Document & { + groupId: Types.ObjectId; + author: Types.ObjectId; + prompt: string; + type: string; +}; + +/** Mirrors the attachProductionPrompts helper from api/models/Prompt.js */ +async function attachProductionPrompts( + groups: Array>, + PromptModel: mongoose.Model, +): Promise>> { + const productionIds = groups.map((g) => g.productionId as Types.ObjectId).filter(Boolean); + + if (productionIds.length === 0) { + return groups.map((g) => ({ ...g, productionPrompt: null })); + } + + const prompts = await PromptModel.find({ _id: { $in: productionIds } }) + .select('prompt') + .lean(); + const promptMap = new Map(prompts.map((p) => [p._id.toString(), p])); + + return groups.map((g) => ({ + ...g, + productionPrompt: g.productionId + ? (promptMap.get((g.productionId as Types.ObjectId).toString()) ?? null) + : null, + })); +} + +describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () => { + let PromptGroup: mongoose.Model; + let Prompt: mongoose.Model; + + const authorId = new Types.ObjectId(); + + beforeAll(async () => { + await mongoose.connect(FERRETDB_URI as string); + PromptGroup = + (mongoose.models.FDBPromptGroup as mongoose.Model) || + mongoose.model('FDBPromptGroup', promptGroupSchema); + Prompt = + (mongoose.models.FDBPrompt as mongoose.Model) || + mongoose.model('FDBPrompt', promptSchema); + await PromptGroup.createCollection(); + await Prompt.createCollection(); + }); + + afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.disconnect(); + }); + + afterEach(async () => { + await PromptGroup.deleteMany({}); + await Prompt.deleteMany({}); + }); + + async function seedGroupWithPrompt( + name: string, + promptText: string, + extra: Record = {}, + ) { + const group = await PromptGroup.create({ + name, + author: authorId, + authorName: 'Test User', + productionId: new Types.ObjectId(), + ...extra, + }); + + const prompt = await Prompt.create({ + groupId: group._id, + author: authorId, + prompt: promptText, + type: 'text', + }); + + await PromptGroup.updateOne({ _id: group._id }, { productionId: prompt._id }); + return { + group: (await PromptGroup.findById(group._id).lean()) as Record, + prompt, + }; + } + + describe('attachProductionPrompts', () => { + it('should attach production prompt text to groups', async () => { + await seedGroupWithPrompt('Group 1', 'Hello {{name}}'); + await seedGroupWithPrompt('Group 2', 'Summarize this: {{text}}'); + + const groups = await PromptGroup.find({}).sort({ name: 1 }).lean(); + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + expect(result).toHaveLength(2); + expect(result[0].name).toBe('Group 1'); + expect((result[0].productionPrompt as Record).prompt).toBe('Hello {{name}}'); + expect(result[1].name).toBe('Group 2'); + expect((result[1].productionPrompt as Record).prompt).toBe( + 'Summarize this: {{text}}', + ); + }); + + it('should handle groups with no productionId', async () => { + await PromptGroup.create({ + name: 'Empty Group', + author: authorId, + authorName: 'Test User', + productionId: null as unknown as Types.ObjectId, + }); + + const groups = await PromptGroup.find({}).lean(); + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + expect(result).toHaveLength(1); + expect(result[0].productionPrompt).toBeNull(); + }); + + it('should handle deleted production prompts gracefully', async () => { + await seedGroupWithPrompt('Orphaned', 'old text'); + await Prompt.deleteMany({}); + + const groups = await PromptGroup.find({}).lean(); + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + expect(result).toHaveLength(1); + expect(result[0].productionPrompt).toBeNull(); + }); + + it('should preserve productionId as the ObjectId (not overwritten)', async () => { + const { prompt } = await seedGroupWithPrompt('Preserved', 'keep id'); + + const groups = await PromptGroup.find({}).lean(); + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + expect((result[0].productionId as Types.ObjectId).toString()).toBe( + (prompt._id as Types.ObjectId).toString(), + ); + expect((result[0].productionPrompt as Record).prompt).toBe('keep id'); + }); + }); + + describe('paginated query pattern (getPromptGroups replacement)', () => { + it('should return paginated groups with production prompts', async () => { + for (let i = 0; i < 5; i++) { + await seedGroupWithPrompt(`Prompt ${i}`, `Content ${i}`); + } + + const query = { author: authorId }; + const skip = 0; + const limit = 3; + + const [groups, total] = await Promise.all([ + PromptGroup.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .select( + 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + ) + .lean(), + PromptGroup.countDocuments(query), + ]); + + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + expect(total).toBe(5); + expect(result).toHaveLength(3); + for (const group of result) { + expect(group.productionPrompt).toBeDefined(); + expect(group.productionPrompt).not.toBeNull(); + } + }); + + it('should correctly compute page count', async () => { + for (let i = 0; i < 7; i++) { + await seedGroupWithPrompt(`Page ${i}`, `Content ${i}`); + } + + const total = await PromptGroup.countDocuments({ author: authorId }); + const pageSize = 3; + const pages = Math.ceil(total / pageSize); + + expect(pages).toBe(3); + }); + }); + + describe('cursor-based pagination pattern (getListPromptGroupsByAccess replacement)', () => { + it('should return groups filtered by accessible IDs with has_more', async () => { + const seeded = []; + for (let i = 0; i < 5; i++) { + const { group } = await seedGroupWithPrompt(`Access ${i}`, `Content ${i}`); + seeded.push(group); + } + + const accessibleIds = seeded.slice(0, 3).map((g) => g._id as Types.ObjectId); + const normalizedLimit = 2; + + const groups = await PromptGroup.find({ _id: { $in: accessibleIds } }) + .sort({ updatedAt: -1, _id: 1 }) + .limit(normalizedLimit + 1) + .select( + 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + ) + .lean(); + + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + const hasMore = result.length > normalizedLimit; + const data = result.slice(0, normalizedLimit); + + expect(hasMore).toBe(true); + expect(data).toHaveLength(2); + for (const group of data) { + expect(group.productionPrompt).not.toBeNull(); + } + }); + + it('should return all groups when no limit is set', async () => { + const seeded = []; + for (let i = 0; i < 4; i++) { + const { group } = await seedGroupWithPrompt(`NoLimit ${i}`, `Content ${i}`); + seeded.push(group); + } + + const accessibleIds = seeded.map((g) => g._id as Types.ObjectId); + const groups = await PromptGroup.find({ _id: { $in: accessibleIds } }) + .sort({ updatedAt: -1, _id: 1 }) + .select( + 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + ) + .lean(); + + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + expect(result).toHaveLength(4); + }); + }); + + describe('output shape matches original $lookup pipeline', () => { + it('should produce the same field structure as the aggregation', async () => { + await seedGroupWithPrompt('Shape Test', 'Check all fields', { + category: 'testing', + oneliner: 'A test prompt', + numberOfGenerations: 5, + }); + + const groups = await PromptGroup.find({}) + .select( + 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + ) + .lean(); + const result = await attachProductionPrompts( + groups as Array>, + Prompt, + ); + + const item = result[0]; + expect(item.name).toBe('Shape Test'); + expect(item.numberOfGenerations).toBe(5); + expect(item.oneliner).toBe('A test prompt'); + expect(item.category).toBe('testing'); + expect(item.projectIds).toEqual([]); + expect(item.productionId).toBeDefined(); + expect(item.author).toBeDefined(); + expect(item.authorName).toBe('Test User'); + expect(item.createdAt).toBeInstanceOf(Date); + expect(item.updatedAt).toBeInstanceOf(Date); + expect(item.productionPrompt).toBeDefined(); + expect((item.productionPrompt as Record).prompt).toBe('Check all fields'); + expect((item.productionPrompt as Record)._id).toBeDefined(); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts new file mode 100644 index 0000000000..446cb701d1 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts @@ -0,0 +1,297 @@ +import mongoose, { Schema, Types } from 'mongoose'; + +/** + * Integration tests for $pullAll compatibility with FerretDB. + * + * These tests verify that the $pull → $pullAll migration works + * identically on both MongoDB and FerretDB by running against + * a real database specified via FERRETDB_URI env var. + * + * Run against FerretDB: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/pullall_test" npx jest pullAll.ferretdb + * + * Run against MongoDB (for parity): + * FERRETDB_URI="mongodb://127.0.0.1:27017/pullall_test" npx jest pullAll.ferretdb + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; + +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const groupSchema = new Schema({ + name: { type: String, required: true }, + memberIds: [{ type: String }], +}); + +const conversationSchema = new Schema({ + conversationId: { type: String, required: true, unique: true }, + user: { type: String }, + tags: { type: [String], default: [] }, +}); + +const projectSchema = new Schema({ + name: { type: String, required: true }, + promptGroupIds: { type: [Schema.Types.ObjectId], default: [] }, + agentIds: { type: [String], default: [] }, +}); + +const agentSchema = new Schema({ + name: { type: String, required: true }, + projectIds: { type: [String], default: [] }, + tool_resources: { type: Schema.Types.Mixed, default: {} }, +}); + +describeIfFerretDB('$pullAll FerretDB compatibility', () => { + let Group: mongoose.Model; + let Conversation: mongoose.Model; + let Project: mongoose.Model; + let Agent: mongoose.Model; + + beforeAll(async () => { + await mongoose.connect(FERRETDB_URI as string); + + Group = mongoose.models.FDBGroup || mongoose.model('FDBGroup', groupSchema); + Conversation = + mongoose.models.FDBConversation || mongoose.model('FDBConversation', conversationSchema); + Project = mongoose.models.FDBProject || mongoose.model('FDBProject', projectSchema); + Agent = mongoose.models.FDBAgent || mongoose.model('FDBAgent', agentSchema); + + await Group.createCollection(); + await Conversation.createCollection(); + await Project.createCollection(); + await Agent.createCollection(); + }); + + afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.disconnect(); + }); + + afterEach(async () => { + await Group.deleteMany({}); + await Conversation.deleteMany({}); + await Project.deleteMany({}); + await Agent.deleteMany({}); + }); + + describe('scalar $pullAll (single value wrapped in array)', () => { + it('should remove a single memberId from a group', async () => { + const userId = new Types.ObjectId().toString(); + const otherUserId = new Types.ObjectId().toString(); + + await Group.create({ + name: 'Test Group', + memberIds: [userId, otherUserId], + }); + + await Group.updateMany({ memberIds: userId }, { $pullAll: { memberIds: [userId] } }); + + const updated = await Group.findOne({ name: 'Test Group' }).lean(); + const doc = updated as Record; + expect(doc.memberIds).toEqual([otherUserId]); + }); + + it('should remove a memberId from multiple groups at once', async () => { + const userId = new Types.ObjectId().toString(); + + await Group.create([ + { name: 'Group A', memberIds: [userId, 'other-1'] }, + { name: 'Group B', memberIds: [userId, 'other-2'] }, + { name: 'Group C', memberIds: ['other-3'] }, + ]); + + await Group.updateMany({ memberIds: userId }, { $pullAll: { memberIds: [userId] } }); + + const groups = await Group.find({}).sort({ name: 1 }).lean(); + const docs = groups as Array>; + expect(docs[0].memberIds).toEqual(['other-1']); + expect(docs[1].memberIds).toEqual(['other-2']); + expect(docs[2].memberIds).toEqual(['other-3']); + }); + + it('should remove a tag from conversations', async () => { + const user = 'user-123'; + const tag = 'important'; + + await Conversation.create([ + { conversationId: 'conv-1', user, tags: [tag, 'other'] }, + { conversationId: 'conv-2', user, tags: [tag] }, + { conversationId: 'conv-3', user, tags: ['other'] }, + ]); + + await Conversation.updateMany({ user, tags: tag }, { $pullAll: { tags: [tag] } }); + + const convos = await Conversation.find({}).sort({ conversationId: 1 }).lean(); + const docs = convos as Array>; + expect(docs[0].tags).toEqual(['other']); + expect(docs[1].tags).toEqual([]); + expect(docs[2].tags).toEqual(['other']); + }); + + it('should remove a single agentId from all projects', async () => { + const agentId = 'agent-to-remove'; + + await Project.create([ + { name: 'Proj A', agentIds: [agentId, 'agent-keep'] }, + { name: 'Proj B', agentIds: ['agent-keep'] }, + ]); + + await Project.updateMany({}, { $pullAll: { agentIds: [agentId] } }); + + const projects = await Project.find({}).sort({ name: 1 }).lean(); + const docs = projects as Array>; + expect(docs[0].agentIds).toEqual(['agent-keep']); + expect(docs[1].agentIds).toEqual(['agent-keep']); + }); + + it('should be a no-op when the value does not exist in the array', async () => { + await Group.create({ name: 'Stable Group', memberIds: ['a', 'b'] }); + + await Group.updateMany( + { memberIds: 'nonexistent' }, + { $pullAll: { memberIds: ['nonexistent'] } }, + ); + + const group = await Group.findOne({ name: 'Stable Group' }).lean(); + const doc = group as Record; + expect(doc.memberIds).toEqual(['a', 'b']); + }); + }); + + describe('multi-value $pullAll (replacing $pull + $in)', () => { + it('should remove multiple promptGroupIds from a project', async () => { + const ids = [new Types.ObjectId(), new Types.ObjectId(), new Types.ObjectId()]; + + await Project.create({ + name: 'Test Project', + promptGroupIds: ids, + }); + + const toRemove = [ids[0], ids[2]]; + await Project.findOneAndUpdate( + { name: 'Test Project' }, + { $pullAll: { promptGroupIds: toRemove } }, + { new: true }, + ); + + const updated = await Project.findOne({ name: 'Test Project' }).lean(); + const doc = updated as Record; + const remaining = (doc.promptGroupIds as Types.ObjectId[]).map((id) => id.toString()); + expect(remaining).toEqual([ids[1].toString()]); + }); + + it('should remove multiple agentIds from a project', async () => { + await Project.create({ + name: 'Agent Project', + agentIds: ['a1', 'a2', 'a3', 'a4'], + }); + + await Project.findOneAndUpdate( + { name: 'Agent Project' }, + { $pullAll: { agentIds: ['a1', 'a3'] } }, + { new: true }, + ); + + const updated = await Project.findOne({ name: 'Agent Project' }).lean(); + const doc = updated as Record; + expect(doc.agentIds).toEqual(['a2', 'a4']); + }); + + it('should remove projectIds from an agent', async () => { + await Agent.create({ + name: 'Test Agent', + projectIds: ['p1', 'p2', 'p3'], + }); + + await Agent.findOneAndUpdate( + { name: 'Test Agent' }, + { $pullAll: { projectIds: ['p1', 'p3'] } }, + { new: true }, + ); + + const updated = await Agent.findOne({ name: 'Test Agent' }).lean(); + const doc = updated as Record; + expect(doc.projectIds).toEqual(['p2']); + }); + + it('should handle removing from nested dynamic paths (tool_resources)', async () => { + await Agent.create({ + name: 'Resource Agent', + tool_resources: { + code_interpreter: { file_ids: ['f1', 'f2', 'f3'] }, + file_search: { file_ids: ['f4', 'f5'] }, + }, + }); + + const pullAllOps: Record = {}; + const filesByResource = { + code_interpreter: ['f1', 'f3'], + file_search: ['f5'], + }; + + for (const [resource, fileIds] of Object.entries(filesByResource)) { + pullAllOps[`tool_resources.${resource}.file_ids`] = fileIds; + } + + await Agent.findOneAndUpdate( + { name: 'Resource Agent' }, + { $pullAll: pullAllOps }, + { new: true }, + ); + + const updated = await Agent.findOne({ name: 'Resource Agent' }).lean(); + const doc = updated as unknown as Record; + expect(doc.tool_resources.code_interpreter.file_ids).toEqual(['f2']); + expect(doc.tool_resources.file_search.file_ids).toEqual(['f4']); + }); + + it('should handle empty array (no-op)', async () => { + await Project.create({ + name: 'Unchanged', + agentIds: ['a1', 'a2'], + }); + + await Project.findOneAndUpdate( + { name: 'Unchanged' }, + { $pullAll: { agentIds: [] } }, + { new: true }, + ); + + const updated = await Project.findOne({ name: 'Unchanged' }).lean(); + const doc = updated as Record; + expect(doc.agentIds).toEqual(['a1', 'a2']); + }); + + it('should handle values not present in the array', async () => { + await Project.create({ + name: 'Partial', + agentIds: ['a1', 'a2'], + }); + + await Project.findOneAndUpdate( + { name: 'Partial' }, + { $pullAll: { agentIds: ['a1', 'nonexistent'] } }, + { new: true }, + ); + + const updated = await Project.findOne({ name: 'Partial' }).lean(); + const doc = updated as Record; + expect(doc.agentIds).toEqual(['a2']); + }); + }); + + describe('duplicate handling', () => { + it('should remove all occurrences of a duplicated value', async () => { + await Group.create({ + name: 'Dupes Group', + memberIds: ['a', 'b', 'a', 'c', 'a'], + }); + + await Group.updateMany({ name: 'Dupes Group' }, { $pullAll: { memberIds: ['a'] } }); + + const updated = await Group.findOne({ name: 'Dupes Group' }).lean(); + const doc = updated as Record; + expect(doc.memberIds).toEqual(['b', 'c']); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/pullSubdocument.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/pullSubdocument.ferretdb.spec.ts new file mode 100644 index 0000000000..6a7651b055 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/pullSubdocument.ferretdb.spec.ts @@ -0,0 +1,199 @@ +import mongoose, { Schema } from 'mongoose'; + +/** + * Integration tests to verify whether $pull with condition objects + * works on FerretDB v2.x. The v1.24 docs listed $pull as supported, + * but the v2.x array update operator docs only list $push, $addToSet, + * $pop, and $pullAll. + * + * This test covers the 3 patterns used in api/models/Agent.js: + * 1. $pull { edges: { to: id } } -- simple condition object + * 2. $pull { favorites: { agentId: id } } -- single scalar match + * 3. $pull { favorites: { agentId: { $in: [...] } } } -- $in condition + * + * Run against FerretDB: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/pull_subdoc_test" npx jest pullSubdocument.ferretdb + * + * Run against MongoDB (for parity): + * FERRETDB_URI="mongodb://127.0.0.1:27017/pull_subdoc_test" npx jest pullSubdocument.ferretdb + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const agentSchema = new Schema({ + name: { type: String, required: true }, + edges: { type: [Schema.Types.Mixed], default: [] }, +}); + +const userSchema = new Schema({ + name: { type: String, required: true }, + favorites: { + type: [ + { + _id: false, + agentId: String, + model: String, + endpoint: String, + }, + ], + default: [], + }, +}); + +type AgentDoc = mongoose.InferSchemaType; +type UserDoc = mongoose.InferSchemaType; + +describeIfFerretDB('$pull with condition objects - FerretDB v2 verification', () => { + let Agent: mongoose.Model; + let User: mongoose.Model; + + beforeAll(async () => { + await mongoose.connect(FERRETDB_URI as string); + Agent = mongoose.model('TestPullAgent', agentSchema); + User = mongoose.model('TestPullUser', userSchema); + }); + + afterAll(async () => { + await mongoose.connection.db?.dropDatabase(); + await mongoose.disconnect(); + }); + + beforeEach(async () => { + await Agent.deleteMany({}); + await User.deleteMany({}); + }); + + describe('Pattern 1: $pull { edges: { to: id } }', () => { + it('should remove edge subdocuments matching a condition', async () => { + await Agent.create({ + name: 'Agent A', + edges: [ + { from: 'a', to: 'b', edgeType: 'handoff' }, + { from: 'a', to: 'c', edgeType: 'direct' }, + { from: 'a', to: 'b', edgeType: 'direct' }, + ], + }); + + await Agent.updateMany({ 'edges.to': 'b' }, { $pull: { edges: { to: 'b' } } }); + + const result = await Agent.findOne({ name: 'Agent A' }).lean(); + expect(result?.edges).toHaveLength(1); + expect((result?.edges[0] as Record).to).toBe('c'); + }); + + it('should not affect agents without matching edges', async () => { + await Agent.create({ + name: 'Agent B', + edges: [{ from: 'x', to: 'y' }], + }); + + await Agent.updateMany({ 'edges.to': 'z' }, { $pull: { edges: { to: 'z' } } }); + + const result = await Agent.findOne({ name: 'Agent B' }).lean(); + expect(result?.edges).toHaveLength(1); + }); + }); + + describe('Pattern 2: $pull { favorites: { agentId: id } }', () => { + it('should remove favorite subdocuments matching agentId', async () => { + await User.create({ + name: 'User 1', + favorites: [ + { agentId: 'agent_1' }, + { agentId: 'agent_2' }, + { model: 'gpt-4', endpoint: 'openAI' }, + ], + }); + + await User.updateMany( + { 'favorites.agentId': 'agent_1' }, + { $pull: { favorites: { agentId: 'agent_1' } } }, + ); + + const result = await User.findOne({ name: 'User 1' }).lean(); + expect(result?.favorites).toHaveLength(2); + + const agentIds = result?.favorites.map((f) => f.agentId).filter(Boolean); + expect(agentIds).toEqual(['agent_2']); + }); + + it('should remove from multiple users at once', async () => { + await User.create([ + { + name: 'User A', + favorites: [{ agentId: 'target' }, { agentId: 'keep' }], + }, + { + name: 'User B', + favorites: [{ agentId: 'target' }], + }, + { + name: 'User C', + favorites: [{ agentId: 'keep' }], + }, + ]); + + await User.updateMany( + { 'favorites.agentId': 'target' }, + { $pull: { favorites: { agentId: 'target' } } }, + ); + + const users = await User.find({}).sort({ name: 1 }).lean(); + expect(users[0].favorites).toHaveLength(1); + expect(users[0].favorites[0].agentId).toBe('keep'); + expect(users[1].favorites).toHaveLength(0); + expect(users[2].favorites).toHaveLength(1); + expect(users[2].favorites[0].agentId).toBe('keep'); + }); + }); + + describe('Pattern 3: $pull { favorites: { agentId: { $in: [...] } } }', () => { + it('should remove favorites matching any agentId in the array', async () => { + await User.create({ + name: 'Bulk User', + favorites: [ + { agentId: 'a1' }, + { agentId: 'a2' }, + { agentId: 'a3' }, + { model: 'gpt-4', endpoint: 'openAI' }, + ], + }); + + await User.updateMany( + { 'favorites.agentId': { $in: ['a1', 'a3'] } }, + { $pull: { favorites: { agentId: { $in: ['a1', 'a3'] } } } }, + ); + + const result = await User.findOne({ name: 'Bulk User' }).lean(); + expect(result?.favorites).toHaveLength(2); + + const agentIds = result?.favorites.map((f) => f.agentId).filter(Boolean); + expect(agentIds).toEqual(['a2']); + }); + + it('should work across multiple users with $in', async () => { + await User.create([ + { + name: 'Multi A', + favorites: [{ agentId: 'x' }, { agentId: 'y' }, { agentId: 'z' }], + }, + { + name: 'Multi B', + favorites: [{ agentId: 'x' }, { agentId: 'z' }], + }, + ]); + + await User.updateMany( + { 'favorites.agentId': { $in: ['x', 'y'] } }, + { $pull: { favorites: { agentId: { $in: ['x', 'y'] } } } }, + ); + + const users = await User.find({}).sort({ name: 1 }).lean(); + expect(users[0].favorites).toHaveLength(1); + expect(users[0].favorites[0].agentId).toBe('z'); + expect(users[1].favorites).toHaveLength(1); + expect(users[1].favorites[0].agentId).toBe('z'); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/randomPrompts.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/randomPrompts.ferretdb.spec.ts new file mode 100644 index 0000000000..ccc274d7fc --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/randomPrompts.ferretdb.spec.ts @@ -0,0 +1,210 @@ +import mongoose, { Schema, Types } from 'mongoose'; + +/** + * Integration tests for $sample → app-level shuffle replacement. + * + * The original getRandomPromptGroups used a $sample aggregation stage + * (unsupported by FerretDB). It was replaced with: + * 1. PromptGroup.distinct('category', { category: { $ne: '' } }) + * 2. Fisher-Yates shuffle of the categories array + * 3. PromptGroup.find({ category: { $in: selectedCategories } }) + * 4. Deduplicate (one group per category) and order by shuffled categories + * + * Run against FerretDB: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/random_prompts_test" npx jest randomPrompts.ferretdb + * + * Run against MongoDB (for parity): + * FERRETDB_URI="mongodb://127.0.0.1:27017/random_prompts_test" npx jest randomPrompts.ferretdb + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; + +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const promptGroupSchema = new Schema({ + name: { type: String, required: true }, + category: { type: String, default: '' }, + author: { type: Schema.Types.ObjectId, required: true }, + authorName: { type: String, default: '' }, +}); + +/** Reproduces the refactored getRandomPromptGroups logic */ +async function getRandomPromptGroups( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PromptGroup: mongoose.Model, + filter: { limit: number; skip: number }, +) { + const categories: string[] = await PromptGroup.distinct('category', { category: { $ne: '' } }); + + for (let i = categories.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [categories[i], categories[j]] = [categories[j], categories[i]]; + } + + const skip = +filter.skip; + const limit = +filter.limit; + const selectedCategories = categories.slice(skip, skip + limit); + + if (selectedCategories.length === 0) { + return { prompts: [] }; + } + + const groups = await PromptGroup.find({ category: { $in: selectedCategories } }).lean(); + + const groupByCategory = new Map(); + for (const group of groups) { + const cat = (group as Record).category; + if (!groupByCategory.has(cat)) { + groupByCategory.set(cat, group); + } + } + + const prompts = selectedCategories.map((cat: string) => groupByCategory.get(cat)).filter(Boolean); + + return { prompts }; +} + +describeIfFerretDB('Random prompts $sample replacement - FerretDB compatibility', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let PromptGroup: mongoose.Model; + const authorId = new Types.ObjectId(); + + beforeAll(async () => { + await mongoose.connect(FERRETDB_URI as string); + PromptGroup = mongoose.model('TestRandPromptGroup', promptGroupSchema); + }); + + afterAll(async () => { + await mongoose.connection.db?.dropDatabase(); + await mongoose.disconnect(); + }); + + beforeEach(async () => { + await PromptGroup.deleteMany({}); + }); + + describe('distinct categories + $in query', () => { + it('should return one group per category', async () => { + await PromptGroup.insertMany([ + { name: 'Code A', category: 'code', author: authorId, authorName: 'User' }, + { name: 'Code B', category: 'code', author: authorId, authorName: 'User' }, + { name: 'Write A', category: 'writing', author: authorId, authorName: 'User' }, + { name: 'Write B', category: 'writing', author: authorId, authorName: 'User' }, + { name: 'Math A', category: 'math', author: authorId, authorName: 'User' }, + ]); + + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 0 }); + expect(result.prompts).toHaveLength(3); + + const categories = result.prompts.map((p: Record) => p.category).sort(); + expect(categories).toEqual(['code', 'math', 'writing']); + }); + + it('should exclude groups with empty category', async () => { + await PromptGroup.insertMany([ + { name: 'Has Category', category: 'code', author: authorId, authorName: 'User' }, + { name: 'Empty Category', category: '', author: authorId, authorName: 'User' }, + ]); + + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 0 }); + expect(result.prompts).toHaveLength(1); + expect((result.prompts[0] as Record).name).toBe('Has Category'); + }); + + it('should return empty array when no groups have categories', async () => { + await PromptGroup.insertMany([ + { name: 'No Cat 1', category: '', author: authorId, authorName: 'User' }, + { name: 'No Cat 2', category: '', author: authorId, authorName: 'User' }, + ]); + + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 0 }); + expect(result.prompts).toHaveLength(0); + }); + + it('should return empty array when collection is empty', async () => { + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 0 }); + expect(result.prompts).toHaveLength(0); + }); + }); + + describe('pagination (skip + limit)', () => { + it('should respect limit', async () => { + await PromptGroup.insertMany([ + { name: 'A', category: 'cat1', author: authorId, authorName: 'User' }, + { name: 'B', category: 'cat2', author: authorId, authorName: 'User' }, + { name: 'C', category: 'cat3', author: authorId, authorName: 'User' }, + { name: 'D', category: 'cat4', author: authorId, authorName: 'User' }, + { name: 'E', category: 'cat5', author: authorId, authorName: 'User' }, + ]); + + const result = await getRandomPromptGroups(PromptGroup, { limit: 3, skip: 0 }); + expect(result.prompts).toHaveLength(3); + }); + + it('should respect skip', async () => { + await PromptGroup.insertMany([ + { name: 'A', category: 'cat1', author: authorId, authorName: 'User' }, + { name: 'B', category: 'cat2', author: authorId, authorName: 'User' }, + { name: 'C', category: 'cat3', author: authorId, authorName: 'User' }, + { name: 'D', category: 'cat4', author: authorId, authorName: 'User' }, + ]); + + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 2 }); + expect(result.prompts).toHaveLength(2); + }); + + it('should return empty when skip exceeds total categories', async () => { + await PromptGroup.insertMany([ + { name: 'A', category: 'cat1', author: authorId, authorName: 'User' }, + { name: 'B', category: 'cat2', author: authorId, authorName: 'User' }, + ]); + + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 5 }); + expect(result.prompts).toHaveLength(0); + }); + }); + + describe('randomness', () => { + it('should produce varying orderings across multiple calls', async () => { + const categories = Array.from({ length: 10 }, (_, i) => `cat_${i}`); + await PromptGroup.insertMany( + categories.map((cat) => ({ + name: cat, + category: cat, + author: authorId, + authorName: 'User', + })), + ); + + const orderings = new Set(); + for (let i = 0; i < 20; i++) { + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 0 }); + const order = result.prompts.map((p: Record) => p.category).join(','); + orderings.add(order); + } + + expect(orderings.size).toBeGreaterThan(1); + }); + }); + + describe('deduplication correctness', () => { + it('should return exactly one group per category even with many duplicates', async () => { + const docs = []; + for (let i = 0; i < 50; i++) { + docs.push({ + name: `Group ${i}`, + category: `cat_${i % 5}`, + author: authorId, + authorName: 'User', + }); + } + await PromptGroup.insertMany(docs); + + const result = await getRandomPromptGroups(PromptGroup, { limit: 10, skip: 0 }); + expect(result.prompts).toHaveLength(5); + + const categories = result.prompts.map((p: Record) => p.category).sort(); + expect(categories).toEqual(['cat_0', 'cat_1', 'cat_2', 'cat_3', 'cat_4']); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/sharding.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/sharding.ferretdb.spec.ts new file mode 100644 index 0000000000..e27e0bbe09 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/sharding.ferretdb.spec.ts @@ -0,0 +1,522 @@ +import mongoose, { Schema, type Connection, type Model } from 'mongoose'; +import { + actionSchema, + agentSchema, + agentApiKeySchema, + agentCategorySchema, + assistantSchema, + balanceSchema, + bannerSchema, + conversationTagSchema, + convoSchema, + fileSchema, + keySchema, + messageSchema, + pluginAuthSchema, + presetSchema, + projectSchema, + promptSchema, + promptGroupSchema, + roleSchema, + sessionSchema, + shareSchema, + tokenSchema, + toolCallSchema, + transactionSchema, + userSchema, + memorySchema, + groupSchema, +} from '~/schema'; +import accessRoleSchema from '~/schema/accessRole'; +import aclEntrySchema from '~/schema/aclEntry'; +import mcpServerSchema from '~/schema/mcpServer'; + +/** + * Sharding PoC — self-contained proof-of-concept that exercises: + * 1. Multi-pool connection management via mongoose.createConnection() + * 2. Persistent org→pool assignment table with capacity limits + * 3. Lazy per-org model registration using all 29 LibreChat schemas + * 4. Cross-pool data isolation + * 5. Routing overhead measurement + * 6. Capacity overflow handling + * + * Both "pools" point to the same FerretDB for the PoC. + * In production each pool URI would be a separate FerretDB+Postgres pair. + * + * Run: + * FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/shard_poc" \ + * npx jest sharding.ferretdb --testTimeout=120000 + */ + +const FERRETDB_URI = process.env.FERRETDB_URI; +const describeIfFerretDB = FERRETDB_URI ? describe : describe.skip; + +const DB_PREFIX = 'shard_poc_'; + +// ─── TYPES ────────────────────────────────────────────────────────────────── + +interface PoolConfig { + id: string; + uri: string; + maxOrgs: number; +} + +interface PoolStats { + orgCount: number; + maxOrgs: number; + available: number; +} + +// ─── ALL 29 LIBRECHAT SCHEMAS ─────────────────────────────────────────────── + +const MODEL_SCHEMAS: Record = { + User: userSchema, + Token: tokenSchema, + Session: sessionSchema, + Balance: balanceSchema, + Conversation: convoSchema, + Message: messageSchema, + Agent: agentSchema, + AgentApiKey: agentApiKeySchema, + AgentCategory: agentCategorySchema, + MCPServer: mcpServerSchema, + Role: roleSchema, + Action: actionSchema, + Assistant: assistantSchema, + File: fileSchema, + Banner: bannerSchema, + Project: projectSchema, + Key: keySchema, + PluginAuth: pluginAuthSchema, + Transaction: transactionSchema, + Preset: presetSchema, + Prompt: promptSchema, + PromptGroup: promptGroupSchema, + ConversationTag: conversationTagSchema, + SharedLink: shareSchema, + ToolCall: toolCallSchema, + MemoryEntry: memorySchema, + AccessRole: accessRoleSchema, + AclEntry: aclEntrySchema, + Group: groupSchema, +}; + +const MODEL_COUNT = Object.keys(MODEL_SCHEMAS).length; + +// ─── TENANT ROUTER (INLINE POC) ──────────────────────────────────────────── + +const assignmentSchema = new Schema({ + orgId: { type: String, required: true, unique: true, index: true }, + poolId: { type: String, required: true, index: true }, + createdAt: { type: Date, default: Date.now }, +}); + +class TenantRouter { + private pools: PoolConfig[] = []; + private poolConns = new Map(); + private orgConns = new Map(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private orgModels = new Map>>(); + private assignmentCache = new Map(); + private controlConn!: Connection; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private Assignment!: Model; + + async initialize(pools: PoolConfig[], controlUri: string): Promise { + this.pools = pools; + + this.controlConn = await mongoose.createConnection(controlUri).asPromise(); + this.Assignment = this.controlConn.model('OrgAssignment', assignmentSchema); + await this.Assignment.createCollection(); + await this.Assignment.createIndexes(); + + for (const pool of pools) { + const conn = await mongoose.createConnection(pool.uri).asPromise(); + this.poolConns.set(pool.id, conn); + } + } + + /** Resolve orgId → Mongoose Connection for that org's database */ + async getOrgConnection(orgId: string): Promise { + const cached = this.orgConns.get(orgId); + if (cached) { + return cached; + } + + const poolId = await this.resolvePool(orgId); + const poolConn = this.poolConns.get(poolId); + if (!poolConn) { + throw new Error(`Pool ${poolId} not configured`); + } + + const orgConn = poolConn.useDb(`${DB_PREFIX}org_${orgId}`, { useCache: true }); + this.orgConns.set(orgId, orgConn); + return orgConn; + } + + /** Get all 29 models registered on an org's connection (lazy) */ + async getOrgModels(orgId: string): Promise>> { + const cached = this.orgModels.get(orgId); + if (cached) { + return cached; + } + + const conn = await this.getOrgConnection(orgId); + const models: Record> = {}; + for (const [name, schema] of Object.entries(MODEL_SCHEMAS)) { + models[name] = conn.models[name] || conn.model(name, schema); + } + this.orgModels.set(orgId, models); + return models; + } + + /** Convenience: get a single model for an org */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getModel(orgId: string, modelName: string): Promise> { + const models = await this.getOrgModels(orgId); + const model = models[modelName]; + if (!model) { + throw new Error(`Unknown model: ${modelName}`); + } + return model; + } + + /** Provision a new org: create all collections + indexes (with deadlock retry) */ + async initializeOrg(orgId: string): Promise { + const models = await this.getOrgModels(orgId); + const t0 = Date.now(); + for (const model of Object.values(models)) { + await model.createCollection(); + for (let attempt = 0; attempt < 3; attempt++) { + try { + await model.createIndexes(); + break; + } catch (err: unknown) { + const msg = (err as Error).message || ''; + if (msg.includes('deadlock') && attempt < 2) { + await new Promise((r) => setTimeout(r, 50 * (attempt + 1))); + continue; + } + throw err; + } + } + } + return Date.now() - t0; + } + + /** Assign org to a pool with capacity, or return existing assignment */ + async assignOrg(orgId: string): Promise { + const cached = this.assignmentCache.get(orgId); + if (cached) { + return cached; + } + + const existing = (await this.Assignment.findOne({ orgId }).lean()) as Record< + string, + unknown + > | null; + if (existing) { + const poolId = existing.poolId as string; + this.assignmentCache.set(orgId, poolId); + return poolId; + } + + const poolId = await this.selectPoolWithCapacity(); + + try { + await this.Assignment.create({ orgId, poolId }); + } catch (err: unknown) { + if ((err as Record).code === 11000) { + const doc = (await this.Assignment.findOne({ orgId }).lean()) as Record; + const existingPoolId = doc.poolId as string; + this.assignmentCache.set(orgId, existingPoolId); + return existingPoolId; + } + throw err; + } + + this.assignmentCache.set(orgId, poolId); + return poolId; + } + + /** Get per-pool statistics */ + async getPoolStats(): Promise> { + const stats: Record = {}; + for (const pool of this.pools) { + const orgCount = await this.Assignment.countDocuments({ poolId: pool.id }); + stats[pool.id] = { + orgCount, + maxOrgs: pool.maxOrgs, + available: pool.maxOrgs - orgCount, + }; + } + return stats; + } + + /** Which pool is an org on? (for test assertions) */ + getAssignment(orgId: string): string | undefined { + return this.assignmentCache.get(orgId); + } + + /** Drop all org databases and the control database */ + async destroyAll(): Promise { + const assignments = (await this.Assignment.find({}).lean()) as Array>; + + for (const a of assignments) { + const orgId = a.orgId as string; + const conn = this.orgConns.get(orgId); + if (conn) { + try { + await conn.dropDatabase(); + } catch { + /* best-effort */ + } + } + } + + try { + await this.controlConn.dropDatabase(); + } catch { + /* best-effort */ + } + } + + async shutdown(): Promise { + for (const conn of this.poolConns.values()) { + await conn.close(); + } + await this.controlConn.close(); + } + + private async resolvePool(orgId: string): Promise { + return this.assignOrg(orgId); + } + + private async selectPoolWithCapacity(): Promise { + for (const pool of this.pools) { + const count = await this.Assignment.countDocuments({ poolId: pool.id }); + if (count < pool.maxOrgs) { + return pool.id; + } + } + throw new Error('All pools at capacity. Add a new pool.'); + } +} + +// ─── TESTS ────────────────────────────────────────────────────────────────── + +describeIfFerretDB('Sharding PoC', () => { + let router: TenantRouter; + + const POOL_A = 'pool-a'; + const POOL_B = 'pool-b'; + const MAX_PER_POOL = 5; + + beforeAll(async () => { + router = new TenantRouter(); + + await router.initialize( + [ + { id: POOL_A, uri: FERRETDB_URI as string, maxOrgs: MAX_PER_POOL }, + { id: POOL_B, uri: FERRETDB_URI as string, maxOrgs: MAX_PER_POOL }, + ], + FERRETDB_URI as string, + ); + }, 30_000); + + afterAll(async () => { + await router.destroyAll(); + await router.shutdown(); + }, 120_000); + + describe('pool assignment and capacity', () => { + it('assigns first 5 orgs to pool A', async () => { + for (let i = 1; i <= 5; i++) { + const poolId = await router.assignOrg(`org_${i}`); + expect(poolId).toBe(POOL_A); + } + + const stats = await router.getPoolStats(); + expect(stats[POOL_A].orgCount).toBe(5); + expect(stats[POOL_A].available).toBe(0); + expect(stats[POOL_B].orgCount).toBe(0); + }); + + it('spills orgs 6-10 to pool B when pool A is full', async () => { + for (let i = 6; i <= 10; i++) { + const poolId = await router.assignOrg(`org_${i}`); + expect(poolId).toBe(POOL_B); + } + + const stats = await router.getPoolStats(); + expect(stats[POOL_A].orgCount).toBe(5); + expect(stats[POOL_B].orgCount).toBe(5); + }); + + it('throws when all pools are at capacity', async () => { + await expect(router.assignOrg('org_overflow')).rejects.toThrow('All pools at capacity'); + }); + + it('returns existing assignment on duplicate call (idempotent)', async () => { + const first = await router.assignOrg('org_1'); + const second = await router.assignOrg('org_1'); + expect(first).toBe(second); + expect(first).toBe(POOL_A); + }); + }); + + describe('org initialization and model registration', () => { + it('initializes an org with all 29 collections and indexes', async () => { + const ms = await router.initializeOrg('org_1'); + console.log(`[Sharding] org_1 init: ${ms}ms (29 collections + 98 indexes)`); + expect(ms).toBeGreaterThan(0); + }, 60_000); + + it('registers all 29 models lazily on the org connection', async () => { + const models = await router.getOrgModels('org_1'); + expect(Object.keys(models)).toHaveLength(MODEL_COUNT); + + for (const name of Object.keys(MODEL_SCHEMAS)) { + expect(models[name]).toBeDefined(); + expect(models[name].modelName).toBe(name); + } + }); + + it('initializes a second org on pool B', async () => { + const ms = await router.initializeOrg('org_6'); + console.log(`[Sharding] org_6 init: ${ms}ms (pool B)`); + + expect(router.getAssignment('org_1')).toBe(POOL_A); + expect(router.getAssignment('org_6')).toBe(POOL_B); + }, 60_000); + }); + + describe('cross-pool data isolation', () => { + it('inserts data in org_1 (pool A) — invisible from org_6 (pool B)', async () => { + const User1 = await router.getModel('org_1', 'User'); + const User6 = await router.getModel('org_6', 'User'); + + await User1.create({ name: 'Alice', email: 'alice@org1.test', username: 'alice1' }); + await User6.create({ name: 'Bob', email: 'bob@org6.test', username: 'bob6' }); + + const org1Users = await User1.find({}).lean(); + const org6Users = await User6.find({}).lean(); + + expect(org1Users).toHaveLength(1); + expect(org6Users).toHaveLength(1); + expect((org1Users[0] as Record).name).toBe('Alice'); + expect((org6Users[0] as Record).name).toBe('Bob'); + }); + + it('runs queries across orgs on different pools concurrently', async () => { + const Message1 = await router.getModel('org_1', 'Message'); + const Message6 = await router.getModel('org_6', 'Message'); + + await Promise.all([ + Message1.create({ + messageId: 'msg_a1', + conversationId: 'conv_a1', + user: 'user_org1', + sender: 'user', + text: 'hello from org 1', + isCreatedByUser: true, + }), + Message6.create({ + messageId: 'msg_b1', + conversationId: 'conv_b1', + user: 'user_org6', + sender: 'user', + text: 'hello from org 6', + isCreatedByUser: true, + }), + ]); + + const [m1, m6] = await Promise.all([ + Message1.findOne({ messageId: 'msg_a1' }).lean(), + Message6.findOne({ messageId: 'msg_b1' }).lean(), + ]); + + expect((m1 as Record).text).toBe('hello from org 1'); + expect((m6 as Record).text).toBe('hello from org 6'); + }); + }); + + describe('routing performance', () => { + it('measures cache-hit vs cold routing latency', async () => { + const iterations = 100; + + const coldStart = process.hrtime.bigint(); + router['assignmentCache'].delete('org_2'); + router['orgConns'].delete('org_2'); + router['orgModels'].delete('org_2'); + await router.getOrgModels('org_2'); + const coldNs = Number(process.hrtime.bigint() - coldStart) / 1e6; + + const times: number[] = []; + for (let i = 0; i < iterations; i++) { + const t0 = process.hrtime.bigint(); + await router.getOrgModels('org_1'); + times.push(Number(process.hrtime.bigint() - t0) / 1e6); + } + times.sort((a, b) => a - b); + + const avg = times.reduce((s, v) => s + v, 0) / times.length; + const p95 = times[Math.floor(times.length * 0.95)]; + + console.log(`[Sharding] Routing overhead:`); + console.log(` Cold (cache miss + DB lookup + model registration): ${coldNs.toFixed(2)}ms`); + console.log( + ` Warm cache hit (${iterations} iters): avg=${avg.toFixed(4)}ms, p95=${p95.toFixed(4)}ms`, + ); + + expect(avg).toBeLessThan(1); + }); + }); + + describe('bulk provisioning simulation', () => { + it('provisions all 10 assigned orgs with collections + indexes', async () => { + const orgIds = Array.from({ length: 10 }, (_, i) => `org_${i + 1}`); + const results: { orgId: string; pool: string; ms: number }[] = []; + + const totalStart = Date.now(); + for (const orgId of orgIds) { + const pool = router.getAssignment(orgId); + const ms = await router.initializeOrg(orgId); + results.push({ orgId, pool: pool ?? '?', ms }); + } + const totalMs = Date.now() - totalStart; + + console.log(`[Sharding] Bulk provisioned ${orgIds.length} orgs in ${totalMs}ms:`); + const poolATimes = results.filter((r) => r.pool === POOL_A).map((r) => r.ms); + const poolBTimes = results.filter((r) => r.pool === POOL_B).map((r) => r.ms); + const avgA = poolATimes.reduce((s, v) => s + v, 0) / poolATimes.length; + const avgB = poolBTimes.reduce((s, v) => s + v, 0) / poolBTimes.length; + console.log(` Pool A (${poolATimes.length} orgs): avg ${Math.round(avgA)}ms/org`); + console.log(` Pool B (${poolBTimes.length} orgs): avg ${Math.round(avgB)}ms/org`); + console.log(` Total: ${totalMs}ms (${Math.round(totalMs / orgIds.length)}ms/org)`); + + expect(results.every((r) => r.ms > 0)).toBe(true); + }, 120_000); + }); + + describe('simulated Express middleware pattern', () => { + it('demonstrates the request-scoped getModel pattern', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fakeReq = { orgId: 'org_1' } as { + orgId: string; + getModel?: (name: string) => Promise>; + }; + + fakeReq.getModel = (modelName: string) => router.getModel(fakeReq.orgId, modelName); + + const User = await fakeReq.getModel!('User'); + const user = await User.findOne({ email: 'alice@org1.test' }).lean(); + expect((user as Record).name).toBe('Alice'); + + fakeReq.orgId = 'org_6'; + const User6 = await fakeReq.getModel!('User'); + const user6 = await User6.findOne({ email: 'bob@org6.test' }).lean(); + expect((user6 as Record).name).toBe('Bob'); + }); + }); +}); diff --git a/packages/data-schemas/misc/ferretdb/tsconfig.json b/packages/data-schemas/misc/ferretdb/tsconfig.json new file mode 100644 index 0000000000..ddd1855bd4 --- /dev/null +++ b/packages/data-schemas/misc/ferretdb/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "target": "ES2020", + "lib": ["ES2020"], + "baseUrl": "../..", + "paths": { + "~/*": ["./src/*"] + } + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/data-schemas/src/methods/aclEntry.ts b/packages/data-schemas/src/methods/aclEntry.ts index c1848960cc..ff27a7046f 100644 --- a/packages/data-schemas/src/methods/aclEntry.ts +++ b/packages/data-schemas/src/methods/aclEntry.ts @@ -307,7 +307,9 @@ export function createAclEntryMethods(mongoose: typeof import('mongoose')) { } if (removeBits) { - if (!update.$bit) update.$bit = {}; + if (!update.$bit) { + update.$bit = {}; + } const bitUpdate = update.$bit as Record; bitUpdate.permBits = { ...(bitUpdate.permBits as Record), and: ~removeBits }; } diff --git a/packages/data-schemas/src/methods/userGroup.ts b/packages/data-schemas/src/methods/userGroup.ts index bec28343fe..f6b57095dc 100644 --- a/packages/data-schemas/src/methods/userGroup.ts +++ b/packages/data-schemas/src/methods/userGroup.ts @@ -215,7 +215,7 @@ export function createUserGroupMethods(mongoose: typeof import('mongoose')) { const userIdOnTheSource = user.idOnTheSource || userId.toString(); const updatedGroup = await Group.findByIdAndUpdate( groupId, - { $pull: { memberIds: userIdOnTheSource } }, + { $pullAll: { memberIds: [userIdOnTheSource] } }, options, ).lean(); diff --git a/packages/data-schemas/src/utils/retry.ts b/packages/data-schemas/src/utils/retry.ts new file mode 100644 index 0000000000..55becf76ac --- /dev/null +++ b/packages/data-schemas/src/utils/retry.ts @@ -0,0 +1,122 @@ +import logger from '~/config/winston'; + +interface RetryOptions { + maxAttempts?: number; + baseDelayMs?: number; + maxDelayMs?: number; + jitter?: boolean; + retryableErrors?: string[]; + onRetry?: (error: Error, attempt: number, delayMs: number) => void; +} + +const DEFAULT_OPTIONS: Required> = { + maxAttempts: 5, + baseDelayMs: 100, + maxDelayMs: 10_000, + jitter: true, + retryableErrors: ['deadlock', 'lock timeout', 'write conflict', 'ECONNRESET'], +}; + +/** + * Executes an async operation with exponential backoff + jitter retry + * on transient errors (deadlocks, connection resets, lock timeouts). + * + * Designed for FerretDB/DocumentDB operations where concurrent index + * creation or bulk writes can trigger PostgreSQL-level deadlocks. + */ +export async function retryWithBackoff( + operation: () => Promise, + label: string, + options: RetryOptions = {}, +): Promise { + const { + maxAttempts = DEFAULT_OPTIONS.maxAttempts, + baseDelayMs = DEFAULT_OPTIONS.baseDelayMs, + maxDelayMs = DEFAULT_OPTIONS.maxDelayMs, + jitter = DEFAULT_OPTIONS.jitter, + retryableErrors = DEFAULT_OPTIONS.retryableErrors, + } = options; + + if (maxAttempts < 1 || baseDelayMs < 0 || maxDelayMs < 0) { + throw new Error( + `[retryWithBackoff] Invalid options: maxAttempts must be >= 1, delays must be non-negative`, + ); + } + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await operation(); + } catch (err: unknown) { + const message = (err as Error)?.message ?? String(err); + const isRetryable = retryableErrors.some((pattern) => + message.toLowerCase().includes(pattern.toLowerCase()), + ); + + if (!isRetryable || attempt === maxAttempts) { + logger.error( + `[retryWithBackoff] ${label} failed permanently after ${attempt} attempt(s): ${message}`, + ); + throw err; + } + + const exponentialDelay = baseDelayMs * Math.pow(2, attempt - 1); + const jitterMs = jitter ? Math.random() * baseDelayMs : 0; + const delayMs = Math.min(exponentialDelay + jitterMs, maxDelayMs); + + logger.warn( + `[retryWithBackoff] ${label} attempt ${attempt}/${maxAttempts} failed (${message}), retrying in ${Math.round(delayMs)}ms`, + ); + + if (options.onRetry) { + const normalizedError = err instanceof Error ? err : new Error(String(err)); + options.onRetry(normalizedError, attempt, delayMs); + } + + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} + +/** + * Creates all indexes for a Mongoose model with deadlock retry. + * Use this instead of raw `model.createIndexes()` on FerretDB. + */ +export async function createIndexesWithRetry( + model: { createIndexes: () => Promise; modelName: string }, + options: RetryOptions = {}, +): Promise { + await retryWithBackoff( + () => model.createIndexes() as Promise, + `createIndexes(${model.modelName})`, + options, + ); +} + +/** + * Initializes all collections and indexes for a set of models on a connection, + * with per-model deadlock retry. Models are processed sequentially to minimize + * contention on the DocumentDB catalog. + */ +export async function initializeOrgCollections( + models: Record< + string, + { + createCollection: () => Promise; + createIndexes: () => Promise; + modelName: string; + } + >, + options: RetryOptions = {}, +): Promise<{ totalMs: number; perModel: Array<{ name: string; ms: number }> }> { + const perModel: Array<{ name: string; ms: number }> = []; + const t0 = Date.now(); + + for (const model of Object.values(models)) { + const modelStart = Date.now(); + await model.createCollection(); + await createIndexesWithRetry(model, options); + perModel.push({ name: model.modelName, ms: Date.now() - modelStart }); + } + + return { totalMs: Date.now() - t0, perModel }; +} diff --git a/packages/data-schemas/src/utils/transactions.ts b/packages/data-schemas/src/utils/transactions.ts index 26f1f77e7e..b54447ffbf 100644 --- a/packages/data-schemas/src/utils/transactions.ts +++ b/packages/data-schemas/src/utils/transactions.ts @@ -18,10 +18,16 @@ export const supportsTransactions = async ( await mongoose.connection.db?.collection('__transaction_test__').findOne({}, { session }); - await session.abortTransaction(); + await session.commitTransaction(); logger.debug('MongoDB transactions are supported'); return true; } catch (transactionError: unknown) { + try { + await session.abortTransaction(); + } catch (transactionError) { + /** best-effort abort */ + logger.error(`[supportsTransactions] Error aborting transaction:`, transactionError); + } logger.debug( 'MongoDB transactions not supported (transaction error):', (transactionError as Error)?.message || 'Unknown error', From 58f128bee7f40f90f85957138bd49eedb708ab61 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 13 Feb 2026 03:04:15 -0500 Subject: [PATCH 094/111] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20chore:=20Remove?= =?UTF-8?q?=20Deprecated=20Project=20Model=20and=20Associated=20Fields=20(?= =?UTF-8?q?#11773)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove projects and projectIds usage * chore: empty line linting * chore: remove isCollaborative property across agent models and related tests - Removed the isCollaborative property from agent models, controllers, and tests, as it is deprecated in favor of ACL permissions. - Updated related validation schemas and data provider types to reflect this change. - Ensured all references to isCollaborative were stripped from the codebase to maintain consistency and clarity. --- api/models/Agent.js | 85 +-------- api/models/Agent.spec.js | 177 +----------------- api/models/Project.js | 133 ------------- api/models/Prompt.js | 64 +------ api/models/Prompt.spec.js | 9 +- api/models/PromptGroupMigration.spec.js | 29 ++- api/server/controllers/agents/v1.js | 6 - api/server/controllers/agents/v1.spec.js | 29 --- api/server/routes/agents/v1.js | 13 +- api/server/routes/config.js | 6 +- api/server/routes/prompts.js | 11 +- api/server/services/start/migration.js | 3 - .../Prompts/Groups/ChatGroupItem.tsx | 13 +- .../Prompts/Groups/DashGroupItem.tsx | 10 +- client/src/components/Prompts/Groups/List.tsx | 17 +- .../SidePanel/Agents/AgentPanel.test.tsx | 3 +- .../Agents/__tests__/AgentFooter.spec.tsx | 8 - client/src/data-provider/prompts.ts | 7 +- config/migrate-agent-permissions.js | 31 ++- config/migrate-prompt-permissions.js | 27 ++- packages/api/src/agents/migration.ts | 27 +-- packages/api/src/agents/validation.ts | 3 - packages/api/src/middleware/access.spec.ts | 27 +-- packages/api/src/prompts/migration.ts | 32 ++-- packages/api/src/prompts/schemas.spec.ts | 20 -- packages/api/src/prompts/schemas.ts | 4 - packages/data-provider/src/config.ts | 3 - packages/data-provider/src/schemas.ts | 3 - packages/data-provider/src/types.ts | 5 +- .../data-provider/src/types/assistants.ts | 6 - .../migrationAntiJoin.ferretdb.spec.ts | 7 +- .../ferretdb/promptLookup.ferretdb.spec.ts | 11 +- .../misc/ferretdb/pullAll.ferretdb.spec.ts | 18 -- packages/data-schemas/src/models/index.ts | 2 - packages/data-schemas/src/models/project.ts | 8 - packages/data-schemas/src/schema/agent.ts | 9 - packages/data-schemas/src/schema/index.ts | 1 - packages/data-schemas/src/schema/project.ts | 34 ---- .../data-schemas/src/schema/promptGroup.ts | 6 - packages/data-schemas/src/types/agent.ts | 3 - packages/data-schemas/src/types/prompts.ts | 1 - 41 files changed, 94 insertions(+), 817 deletions(-) delete mode 100644 api/models/Project.js delete mode 100644 packages/data-schemas/src/models/project.ts delete mode 100644 packages/data-schemas/src/schema/project.ts diff --git a/api/models/Agent.js b/api/models/Agent.js index 7c35260cd5..1ddc535e7b 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -4,7 +4,6 @@ const { logger } = require('@librechat/data-schemas'); const { getCustomEndpointConfig } = require('@librechat/api'); const { Tools, - SystemRoles, ResourceType, actionDelimiter, isAgentsEndpoint, @@ -12,11 +11,6 @@ const { encodeEphemeralAgentId, } = require('librechat-data-provider'); const { mcp_all, mcp_delimiter } = require('librechat-data-provider').Constants; -const { - removeAgentFromAllProjects, - removeAgentIdsFromProject, - addAgentIdsToProject, -} = require('./Project'); const { getSoleOwnedResourceIds, removeAllPermissions, @@ -294,22 +288,8 @@ const isDuplicateVersion = (updateData, currentData, versions, actionsHash = nul break; } - // Special handling for projectIds (MongoDB ObjectIds) - if (field === 'projectIds') { - const wouldBeIds = wouldBeArr.map((id) => id.toString()).sort(); - const versionIds = lastVersionArr.map((id) => id.toString()).sort(); - - if (!wouldBeIds.every((id, i) => id === versionIds[i])) { - isMatch = false; - break; - } - } // Handle arrays of objects - else if ( - wouldBeArr.length > 0 && - typeof wouldBeArr[0] === 'object' && - wouldBeArr[0] !== null - ) { + if (wouldBeArr.length > 0 && typeof wouldBeArr[0] === 'object' && wouldBeArr[0] !== null) { const sortedWouldBe = [...wouldBeArr].map((item) => JSON.stringify(item)).sort(); const sortedVersion = [...lastVersionArr].map((item) => JSON.stringify(item)).sort(); @@ -590,7 +570,6 @@ const removeAgentResourceFiles = async ({ agent_id, files }) => { const deleteAgent = async (searchParameter) => { const agent = await Agent.findOneAndDelete(searchParameter); if (agent) { - await removeAgentFromAllProjects(agent.id); await Promise.all([ removeAllPermissions({ resourceType: ResourceType.AGENT, @@ -667,8 +646,6 @@ const deleteUserAgents = async (userId) => { const agentIds = allAgents.map((agent) => agent.id); const agentObjectIds = allAgents.map((agent) => agent._id); - await Promise.all(agentIds.map((id) => removeAgentFromAllProjects(id))); - await AclEntry.deleteMany({ resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, resourceId: { $in: agentObjectIds }, @@ -753,7 +730,6 @@ const getListAgentsByAccess = async ({ name: 1, avatar: 1, author: 1, - projectIds: 1, description: 1, updatedAt: 1, category: 1, @@ -798,64 +774,6 @@ const getListAgentsByAccess = async ({ }; }; -/** - * Updates the projects associated with an agent, adding and removing project IDs as specified. - * This function also updates the corresponding projects to include or exclude the agent ID. - * - * @param {Object} params - Parameters for updating the agent's projects. - * @param {IUser} params.user - Parameters for updating the agent's projects. - * @param {string} params.agentId - The ID of the agent to update. - * @param {string[]} [params.projectIds] - Array of project IDs to add to the agent. - * @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent. - * @returns {Promise} The updated agent document. - * @throws {Error} If there's an error updating the agent or projects. - */ -const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds }) => { - const updateOps = {}; - - if (removeProjectIds && removeProjectIds.length > 0) { - for (const projectId of removeProjectIds) { - await removeAgentIdsFromProject(projectId, [agentId]); - } - updateOps.$pullAll = { projectIds: removeProjectIds }; - } - - if (projectIds && projectIds.length > 0) { - for (const projectId of projectIds) { - await addAgentIdsToProject(projectId, [agentId]); - } - updateOps.$addToSet = { projectIds: { $each: projectIds } }; - } - - if (Object.keys(updateOps).length === 0) { - return await getAgent({ id: agentId }); - } - - const updateQuery = { id: agentId, author: user.id }; - if (user.role === SystemRoles.ADMIN) { - delete updateQuery.author; - } - - const updatedAgent = await updateAgent(updateQuery, updateOps, { - updatingUserId: user.id, - skipVersioning: true, - }); - if (updatedAgent) { - return updatedAgent; - } - if (updateOps.$addToSet) { - for (const projectId of projectIds) { - await removeAgentIdsFromProject(projectId, [agentId]); - } - } else if (updateOps.$pull) { - for (const projectId of removeProjectIds) { - await addAgentIdsToProject(projectId, [agentId]); - } - } - - return await getAgent({ id: agentId }); -}; - /** * Reverts an agent to a specific version in its version history. * @param {Object} searchParameter - The search parameters to find the agent to revert. @@ -964,7 +882,6 @@ module.exports = { deleteAgent, deleteUserAgents, revertAgentVersion, - updateAgentProjects, countPromotedAgents, addAgentResourceFile, getListAgentsByAccess, diff --git a/api/models/Agent.spec.js b/api/models/Agent.spec.js index b2597872ab..ba2991cff7 100644 --- a/api/models/Agent.spec.js +++ b/api/models/Agent.spec.js @@ -29,7 +29,6 @@ const { deleteAgent, deleteUserAgents, revertAgentVersion, - updateAgentProjects, addAgentResourceFile, getListAgentsByAccess, removeAgentResourceFiles, @@ -1195,53 +1194,6 @@ describe('models/Agent', () => { expect(await getAgent({ id: legacyAgentId })).toBeNull(); }); - test('should update agent projects', async () => { - const agentId = `agent_${uuidv4()}`; - const authorId = new mongoose.Types.ObjectId(); - const projectId1 = new mongoose.Types.ObjectId(); - const projectId2 = new mongoose.Types.ObjectId(); - const projectId3 = new mongoose.Types.ObjectId(); - - await createAgent({ - id: agentId, - name: 'Project Test Agent', - provider: 'test', - model: 'test-model', - author: authorId, - projectIds: [projectId1], - }); - - await updateAgent( - { id: agentId }, - { $addToSet: { projectIds: { $each: [projectId2, projectId3] } } }, - ); - - await updateAgent({ id: agentId }, { $pull: { projectIds: projectId1 } }); - - await updateAgent({ id: agentId }, { projectIds: [projectId2, projectId3] }); - - const updatedAgent = await getAgent({ id: agentId }); - expect(updatedAgent.projectIds).toHaveLength(2); - expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); - expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId3.toString()); - expect(updatedAgent.projectIds.map((id) => id.toString())).not.toContain( - projectId1.toString(), - ); - - await updateAgent({ id: agentId }, { projectIds: [] }); - - const emptyProjectsAgent = await getAgent({ id: agentId }); - expect(emptyProjectsAgent.projectIds).toHaveLength(0); - - const nonExistentId = `agent_${uuidv4()}`; - await expect( - updateAgentProjects({ - id: nonExistentId, - projectIds: [projectId1], - }), - ).rejects.toThrow(); - }); - test('should handle ephemeral agent loading', async () => { const agentId = 'ephemeral_test'; const endpoint = 'openai'; @@ -1313,20 +1265,6 @@ describe('models/Agent', () => { const result = await fn(); expect(result).toBe(expected); }); - - test('should handle updateAgentProjects with non-existent agent', async () => { - const nonExistentId = `agent_${uuidv4()}`; - const userId = new mongoose.Types.ObjectId(); - const projectId = new mongoose.Types.ObjectId(); - - const result = await updateAgentProjects({ - user: { id: userId.toString() }, - agentId: nonExistentId, - projectIds: [projectId.toString()], - }); - - expect(result).toBeNull(); - }); }); }); @@ -1450,7 +1388,6 @@ describe('models/Agent', () => { test('should handle MongoDB operators and field updates correctly', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); - const projectId = new mongoose.Types.ObjectId(); await createAgent({ id: agentId, @@ -1466,7 +1403,6 @@ describe('models/Agent', () => { { description: 'Updated description', $push: { tools: 'tool2' }, - $addToSet: { projectIds: projectId }, }, ); @@ -1474,7 +1410,6 @@ describe('models/Agent', () => { expect(firstUpdate.description).toBe('Updated description'); expect(firstUpdate.tools).toContain('tool1'); expect(firstUpdate.tools).toContain('tool2'); - expect(firstUpdate.projectIds.map((id) => id.toString())).toContain(projectId.toString()); expect(firstUpdate.versions).toHaveLength(2); await updateAgent( @@ -1879,7 +1814,6 @@ describe('models/Agent', () => { test('should handle version comparison with special field types', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); - const projectId = new mongoose.Types.ObjectId(); await createAgent({ id: agentId, @@ -1887,7 +1821,6 @@ describe('models/Agent', () => { provider: 'test', model: 'test-model', author: authorId, - projectIds: [projectId], model_parameters: { temperature: 0.7 }, }); @@ -2765,7 +2698,6 @@ describe('models/Agent', () => { const authorId = new mongoose.Types.ObjectId(); const userId = new mongoose.Types.ObjectId(); const agentId = `agent_${uuidv4()}`; - const projectId = new mongoose.Types.ObjectId(); await createAgent({ id: agentId, @@ -2773,7 +2705,6 @@ describe('models/Agent', () => { provider: 'openai', model: 'gpt-4', author: authorId, - projectIds: [projectId], }); const mockReq = { user: { id: userId.toString() } }; @@ -2833,7 +2764,6 @@ describe('models/Agent', () => { test('should handle agent creation with all optional fields', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); - const projectId = new mongoose.Types.ObjectId(); const agent = await createAgent({ id: agentId, @@ -2846,9 +2776,7 @@ describe('models/Agent', () => { tools: ['tool1', 'tool2'], actions: ['action1', 'action2'], model_parameters: { temperature: 0.8, max_tokens: 1000 }, - projectIds: [projectId], avatar: 'https://example.com/avatar.png', - isCollaborative: true, tool_resources: { file_search: { file_ids: ['file1', 'file2'] }, }, @@ -2862,9 +2790,7 @@ describe('models/Agent', () => { expect(agent.actions).toEqual(['action1', 'action2']); expect(agent.model_parameters.temperature).toBe(0.8); expect(agent.model_parameters.max_tokens).toBe(1000); - expect(agent.projectIds.map((id) => id.toString())).toContain(projectId.toString()); expect(agent.avatar).toBe('https://example.com/avatar.png'); - expect(agent.isCollaborative).toBe(true); expect(agent.tool_resources.file_search.file_ids).toEqual(['file1', 'file2']); }); @@ -3070,21 +2996,6 @@ describe('models/Agent', () => { expect(finalAgent.name).toBe('Version 4'); }); - test('should handle updateAgentProjects error scenarios', async () => { - const nonExistentId = `agent_${uuidv4()}`; - const userId = new mongoose.Types.ObjectId(); - const projectId = new mongoose.Types.ObjectId(); - - // Test with non-existent agent - const result = await updateAgentProjects({ - user: { id: userId.toString() }, - agentId: nonExistentId, - projectIds: [projectId.toString()], - }); - - expect(result).toBeNull(); - }); - test('should handle revertAgentVersion properly', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); @@ -3138,8 +3049,6 @@ describe('models/Agent', () => { test('should handle updateAgent with combined MongoDB operators', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); - const projectId1 = new mongoose.Types.ObjectId(); - const projectId2 = new mongoose.Types.ObjectId(); await createAgent({ id: agentId, @@ -3148,7 +3057,6 @@ describe('models/Agent', () => { model: 'test-model', author: authorId, tools: ['tool1'], - projectIds: [projectId1], }); // Use multiple operators in single update - but avoid conflicting operations on same field @@ -3157,14 +3065,6 @@ describe('models/Agent', () => { { name: 'Updated Name', $push: { tools: 'tool2' }, - $addToSet: { projectIds: projectId2 }, - }, - ); - - const finalAgent = await updateAgent( - { id: agentId }, - { - $pull: { projectIds: projectId1 }, }, ); @@ -3172,11 +3072,7 @@ describe('models/Agent', () => { expect(updatedAgent.name).toBe('Updated Name'); expect(updatedAgent.tools).toContain('tool1'); expect(updatedAgent.tools).toContain('tool2'); - expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); - - expect(finalAgent).toBeDefined(); - expect(finalAgent.projectIds.map((id) => id.toString())).not.toContain(projectId1.toString()); - expect(finalAgent.versions).toHaveLength(3); + expect(updatedAgent.versions).toHaveLength(2); }); test('should handle updateAgent when agent does not exist', async () => { @@ -3450,65 +3346,6 @@ describe('models/Agent', () => { expect(updated2.description).toBe('Another description'); }); - test('should skip version creation when skipVersioning option is used', async () => { - const agentId = `agent_${uuidv4()}`; - const authorId = new mongoose.Types.ObjectId(); - const projectId1 = new mongoose.Types.ObjectId(); - const projectId2 = new mongoose.Types.ObjectId(); - - // Create agent with initial projectIds - await createAgent({ - id: agentId, - name: 'Test Agent', - provider: 'test', - model: 'test-model', - author: authorId, - projectIds: [projectId1], - }); - - // Share agent using updateAgentProjects (which uses skipVersioning) - const shared = await updateAgentProjects({ - user: { id: authorId.toString() }, // Use the same author ID - agentId: agentId, - projectIds: [projectId2.toString()], - }); - - // Should NOT create a new version due to skipVersioning - expect(shared.versions).toHaveLength(1); - expect(shared.projectIds.map((id) => id.toString())).toContain(projectId1.toString()); - expect(shared.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); - - // Unshare agent using updateAgentProjects - const unshared = await updateAgentProjects({ - user: { id: authorId.toString() }, - agentId: agentId, - removeProjectIds: [projectId1.toString()], - }); - - // Still should NOT create a new version - expect(unshared.versions).toHaveLength(1); - expect(unshared.projectIds.map((id) => id.toString())).not.toContain(projectId1.toString()); - expect(unshared.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); - - // Regular update without skipVersioning should create a version - const regularUpdate = await updateAgent( - { id: agentId }, - { description: 'Updated description' }, - ); - - expect(regularUpdate.versions).toHaveLength(2); - expect(regularUpdate.description).toBe('Updated description'); - - // Direct updateAgent with MongoDB operators should still create versions - const directUpdate = await updateAgent( - { id: agentId }, - { $addToSet: { projectIds: { $each: [projectId1] } } }, - ); - - expect(directUpdate.versions).toHaveLength(3); - expect(directUpdate.projectIds.length).toBe(2); - }); - test('should preserve agent_ids in version history', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); @@ -3889,7 +3726,6 @@ function createTestIds() { return { agentId: `agent_${uuidv4()}`, authorId: new mongoose.Types.ObjectId(), - projectId: new mongoose.Types.ObjectId(), fileId: uuidv4(), }; } @@ -3923,9 +3759,6 @@ function mockFindOneAndUpdateError(errorOnCall = 1) { } function generateVersionTestCases() { - const projectId1 = new mongoose.Types.ObjectId(); - const projectId2 = new mongoose.Types.ObjectId(); - return [ { name: 'simple field update', @@ -3952,13 +3785,5 @@ function generateVersionTestCases() { update: { tools: ['tool2', 'tool3'] }, duplicate: { tools: ['tool2', 'tool3'] }, }, - { - name: 'projectIds update', - initial: { - projectIds: [projectId1], - }, - update: { projectIds: [projectId1, projectId2] }, - duplicate: { projectIds: [projectId2, projectId1] }, - }, ]; } diff --git a/api/models/Project.js b/api/models/Project.js deleted file mode 100644 index dc92348b54..0000000000 --- a/api/models/Project.js +++ /dev/null @@ -1,133 +0,0 @@ -const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; -const { Project } = require('~/db/models'); - -/** - * Retrieve a project by ID and convert the found project document to a plain object. - * - * @param {string} projectId - The ID of the project to find and return as a plain object. - * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. - * @returns {Promise} A plain object representing the project document, or `null` if no project is found. - */ -const getProjectById = async function (projectId, fieldsToSelect = null) { - const query = Project.findById(projectId); - - if (fieldsToSelect) { - query.select(fieldsToSelect); - } - - return await query.lean(); -}; - -/** - * Retrieve a project by name and convert the found project document to a plain object. - * If the project with the given name doesn't exist and the name is "instance", create it and return the lean version. - * - * @param {string} projectName - The name of the project to find or create. - * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. - * @returns {Promise} A plain object representing the project document. - */ -const getProjectByName = async function (projectName, fieldsToSelect = null) { - const query = { name: projectName }; - const update = { $setOnInsert: { name: projectName } }; - const options = { - new: true, - upsert: projectName === GLOBAL_PROJECT_NAME, - lean: true, - select: fieldsToSelect, - }; - - return await Project.findOneAndUpdate(query, update, options); -}; - -/** - * Add an array of prompt group IDs to a project's promptGroupIds array, ensuring uniqueness. - * - * @param {string} projectId - The ID of the project to update. - * @param {string[]} promptGroupIds - The array of prompt group IDs to add to the project. - * @returns {Promise} The updated project document. - */ -const addGroupIdsToProject = async function (projectId, promptGroupIds) { - return await Project.findByIdAndUpdate( - projectId, - { $addToSet: { promptGroupIds: { $each: promptGroupIds } } }, - { new: true }, - ); -}; - -/** - * Remove an array of prompt group IDs from a project's promptGroupIds array. - * - * @param {string} projectId - The ID of the project to update. - * @param {string[]} promptGroupIds - The array of prompt group IDs to remove from the project. - * @returns {Promise} The updated project document. - */ -const removeGroupIdsFromProject = async function (projectId, promptGroupIds) { - return await Project.findByIdAndUpdate( - projectId, - { $pullAll: { promptGroupIds: promptGroupIds } }, - { new: true }, - ); -}; - -/** - * Remove a prompt group ID from all projects. - * - * @param {string} promptGroupId - The ID of the prompt group to remove from projects. - * @returns {Promise} - */ -const removeGroupFromAllProjects = async (promptGroupId) => { - await Project.updateMany({}, { $pullAll: { promptGroupIds: [promptGroupId] } }); -}; - -/** - * Add an array of agent IDs to a project's agentIds array, ensuring uniqueness. - * - * @param {string} projectId - The ID of the project to update. - * @param {string[]} agentIds - The array of agent IDs to add to the project. - * @returns {Promise} The updated project document. - */ -const addAgentIdsToProject = async function (projectId, agentIds) { - return await Project.findByIdAndUpdate( - projectId, - { $addToSet: { agentIds: { $each: agentIds } } }, - { new: true }, - ); -}; - -/** - * Remove an array of agent IDs from a project's agentIds array. - * - * @param {string} projectId - The ID of the project to update. - * @param {string[]} agentIds - The array of agent IDs to remove from the project. - * @returns {Promise} The updated project document. - */ -const removeAgentIdsFromProject = async function (projectId, agentIds) { - return await Project.findByIdAndUpdate( - projectId, - { $pullAll: { agentIds: agentIds } }, - { new: true }, - ); -}; - -/** - * Remove an agent ID from all projects. - * - * @param {string} agentId - The ID of the agent to remove from projects. - * @returns {Promise} - */ -const removeAgentFromAllProjects = async (agentId) => { - await Project.updateMany({}, { $pullAll: { agentIds: [agentId] } }); -}; - -module.exports = { - getProjectById, - getProjectByName, - /* prompts */ - addGroupIdsToProject, - removeGroupIdsFromProject, - removeGroupFromAllProjects, - /* agents */ - addAgentIdsToProject, - removeAgentIdsFromProject, - removeAgentFromAllProjects, -}; diff --git a/api/models/Prompt.js b/api/models/Prompt.js index b384c06132..38d56b53a4 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -1,18 +1,7 @@ const { ObjectId } = require('mongodb'); const { escapeRegExp } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); -const { - Constants, - SystemRoles, - ResourceType, - SystemCategories, -} = require('librechat-data-provider'); -const { - removeGroupFromAllProjects, - removeGroupIdsFromProject, - addGroupIdsToProject, - getProjectByName, -} = require('./Project'); +const { SystemRoles, ResourceType, SystemCategories } = require('librechat-data-provider'); const { getSoleOwnedResourceIds, removeAllPermissions, @@ -51,34 +40,21 @@ const getAllPromptGroups = async (req, filter) => { try { const { name, ...query } = filter; - let searchShared = true; - let searchSharedOnly = false; if (name) { query.name = new RegExp(escapeRegExp(name), 'i'); } if (!query.category) { delete query.category; } else if (query.category === SystemCategories.MY_PROMPTS) { - searchShared = false; delete query.category; } else if (query.category === SystemCategories.NO_CATEGORY) { query.category = ''; } else if (query.category === SystemCategories.SHARED_PROMPTS) { - searchSharedOnly = true; delete query.category; } let combinedQuery = query; - if (searchShared) { - const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds'); - if (project && project.promptGroupIds && project.promptGroupIds.length > 0) { - const projectQuery = { _id: { $in: project.promptGroupIds }, ...query }; - delete projectQuery.author; - combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] }; - } - } - const groups = await PromptGroup.find(combinedQuery) .sort({ createdAt: -1 }) .select('name oneliner category author authorName createdAt updatedAt command productionId') @@ -103,34 +79,21 @@ const getPromptGroups = async (req, filter) => { const validatedPageNumber = Math.max(parseInt(pageNumber, 10), 1); const validatedPageSize = Math.max(parseInt(pageSize, 10), 1); - let searchShared = true; - let searchSharedOnly = false; if (name) { query.name = new RegExp(escapeRegExp(name), 'i'); } if (!query.category) { delete query.category; } else if (query.category === SystemCategories.MY_PROMPTS) { - searchShared = false; delete query.category; } else if (query.category === SystemCategories.NO_CATEGORY) { query.category = ''; } else if (query.category === SystemCategories.SHARED_PROMPTS) { - searchSharedOnly = true; delete query.category; } let combinedQuery = query; - if (searchShared) { - const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds'); - if (project && project.promptGroupIds && project.promptGroupIds.length > 0) { - const projectQuery = { _id: { $in: project.promptGroupIds }, ...query }; - delete projectQuery.author; - combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] }; - } - } - const skip = (validatedPageNumber - 1) * validatedPageSize; const limit = validatedPageSize; @@ -140,7 +103,7 @@ const getPromptGroups = async (req, filter) => { .skip(skip) .limit(limit) .select( - 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', ) .lean(), PromptGroup.countDocuments(combinedQuery), @@ -185,7 +148,6 @@ const deletePromptGroup = async ({ _id, author, role }) => { } await Prompt.deleteMany(groupQuery); - await removeGroupFromAllProjects(_id); try { await removeAllPermissions({ resourceType: ResourceType.PROMPTGROUP, resourceId: _id }); @@ -244,7 +206,7 @@ async function getListPromptGroupsByAccess({ const findQuery = PromptGroup.find(baseQuery) .sort({ updatedAt: -1, _id: 1 }) .select( - 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', ); if (isPaginated) { @@ -490,7 +452,6 @@ module.exports = { } await PromptGroup.deleteOne({ _id: groupId }); - await removeGroupFromAllProjects(groupId); return { prompt: 'Prompt deleted successfully', @@ -546,8 +507,6 @@ module.exports = { return; } - await Promise.all(allGroupIdsToDelete.map((id) => removeGroupFromAllProjects(id))); - await AclEntry.deleteMany({ resourceType: ResourceType.PROMPTGROUP, resourceId: { $in: allGroupIdsToDelete }, @@ -568,23 +527,6 @@ module.exports = { updatePromptGroup: async (filter, data) => { try { const updateOps = {}; - if (data.removeProjectIds) { - for (const projectId of data.removeProjectIds) { - await removeGroupIdsFromProject(projectId, [filter._id]); - } - - updateOps.$pullAll = { projectIds: data.removeProjectIds }; - delete data.removeProjectIds; - } - - if (data.projectIds) { - for (const projectId of data.projectIds) { - await addGroupIdsToProject(projectId, [filter._id]); - } - - updateOps.$addToSet = { projectIds: { $each: data.projectIds } }; - delete data.projectIds; - } const updateData = { ...data, ...updateOps }; const updatedDoc = await PromptGroup.findOneAndUpdate(filter, updateData, { diff --git a/api/models/Prompt.spec.js b/api/models/Prompt.spec.js index a2063e6cfc..5c1c8c8256 100644 --- a/api/models/Prompt.spec.js +++ b/api/models/Prompt.spec.js @@ -19,7 +19,7 @@ const dbModels = require('~/db/models'); logger.silent = true; let mongoServer; -let Prompt, PromptGroup, AclEntry, AccessRole, User, Group, Project; +let Prompt, PromptGroup, AclEntry, AccessRole, User, Group; let promptFns, permissionService; let testUsers, testGroups, testRoles; @@ -36,7 +36,6 @@ beforeAll(async () => { AccessRole = dbModels.AccessRole; User = dbModels.User; Group = dbModels.Group; - Project = dbModels.Project; promptFns = require('~/models/Prompt'); permissionService = require('~/server/services/PermissionService'); @@ -118,12 +117,6 @@ async function setupTestData() { description: 'Group with viewer access', }), }; - - await Project.create({ - name: 'Global', - description: 'Global project', - promptGroupIds: [], - }); } describe('Prompt ACL Permissions', () => { diff --git a/api/models/PromptGroupMigration.spec.js b/api/models/PromptGroupMigration.spec.js index f568012cb3..04ff612e7d 100644 --- a/api/models/PromptGroupMigration.spec.js +++ b/api/models/PromptGroupMigration.spec.js @@ -3,7 +3,6 @@ const { ObjectId } = require('mongodb'); const { logger } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { - Constants, ResourceType, AccessRoleIds, PrincipalType, @@ -19,9 +18,9 @@ logger.silent = true; describe('PromptGroup Migration Script', () => { let mongoServer; - let Prompt, PromptGroup, AclEntry, AccessRole, User, Project; + let Prompt, PromptGroup, AclEntry, AccessRole, User; let migrateToPromptGroupPermissions; - let testOwner, testProject; + let testOwner; let ownerRole, viewerRole; beforeAll(async () => { @@ -37,7 +36,6 @@ describe('PromptGroup Migration Script', () => { AclEntry = dbModels.AclEntry; AccessRole = dbModels.AccessRole; User = dbModels.User; - Project = dbModels.Project; // Create test user testOwner = await User.create({ @@ -46,11 +44,10 @@ describe('PromptGroup Migration Script', () => { role: 'USER', }); - // Create test project with the proper name - const projectName = Constants.GLOBAL_PROJECT_NAME || 'instance'; - testProject = await Project.create({ + // Create test project document in the raw `projects` collection + const projectName = 'instance'; + await mongoose.connection.db.collection('projects').insertOne({ name: projectName, - description: 'Global project', promptGroupIds: [], }); @@ -95,9 +92,9 @@ describe('PromptGroup Migration Script', () => { await Prompt.deleteMany({}); await PromptGroup.deleteMany({}); await AclEntry.deleteMany({}); - // Reset the project's promptGroupIds array - testProject.promptGroupIds = []; - await testProject.save(); + await mongoose.connection.db + .collection('projects') + .updateOne({ name: 'instance' }, { $set: { promptGroupIds: [] } }); }); it('should categorize promptGroups correctly in dry run', async () => { @@ -118,8 +115,9 @@ describe('PromptGroup Migration Script', () => { }); // Add global group to project's promptGroupIds array - testProject.promptGroupIds = [globalPromptGroup._id]; - await testProject.save(); + await mongoose.connection.db + .collection('projects') + .updateOne({ name: 'instance' }, { $set: { promptGroupIds: [globalPromptGroup._id] } }); const result = await migrateToPromptGroupPermissions({ dryRun: true }); @@ -146,8 +144,9 @@ describe('PromptGroup Migration Script', () => { }); // Add global group to project's promptGroupIds array - testProject.promptGroupIds = [globalPromptGroup._id]; - await testProject.save(); + await mongoose.connection.db + .collection('projects') + .updateOne({ name: 'instance' }, { $set: { promptGroupIds: [globalPromptGroup._id] } }); const result = await migrateToPromptGroupPermissions({ dryRun: false }); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 309873e56c..899b561352 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -288,9 +288,6 @@ const getAgentHandler = async (req, res, expandProperties = false) => { agent.author = agent.author.toString(); - // @deprecated - isCollaborative replaced by ACL permissions - agent.isCollaborative = !!agent.isCollaborative; - // Check if agent is public const isPublic = await hasPublicPermission({ resourceType: ResourceType.AGENT, @@ -314,9 +311,6 @@ const getAgentHandler = async (req, res, expandProperties = false) => { author: agent.author, provider: agent.provider, model: agent.model, - projectIds: agent.projectIds, - // @deprecated - isCollaborative replaced by ACL permissions - isCollaborative: agent.isCollaborative, isPublic: agent.isPublic, version: agent.version, // Safe metadata diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 959974bc2d..56bb90675a 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -14,10 +14,6 @@ jest.mock('~/server/services/Config', () => ({ }), })); -jest.mock('~/models/Project', () => ({ - getProjectByName: jest.fn().mockResolvedValue(null), -})); - jest.mock('~/server/services/Files/strategies', () => ({ getStrategyFunctions: jest.fn(), })); @@ -176,7 +172,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { // Unauthorized fields that should be stripped author: new mongoose.Types.ObjectId().toString(), // Should not be able to set author authorName: 'Hacker', // Should be stripped - isCollaborative: true, // Should be stripped on creation versions: [], // Should be stripped _id: new mongoose.Types.ObjectId(), // Should be stripped id: 'custom_agent_id', // Should be overridden @@ -195,7 +190,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { // Verify unauthorized fields were not set expect(createdAgent.author.toString()).toBe(mockReq.user.id); // Should be the request user, not the malicious value expect(createdAgent.authorName).toBeUndefined(); - expect(createdAgent.isCollaborative).toBeFalsy(); expect(createdAgent.versions).toHaveLength(1); // Should have exactly 1 version from creation expect(createdAgent.id).not.toBe('custom_agent_id'); // Should have generated ID expect(createdAgent.id).toMatch(/^agent_/); // Should have proper prefix @@ -446,7 +440,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { model: 'gpt-3.5-turbo', author: existingAgentAuthorId, description: 'Original description', - isCollaborative: false, versions: [ { name: 'Original Agent', @@ -468,7 +461,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { name: 'Updated Agent', description: 'Updated description', model: 'gpt-4', - isCollaborative: true, // This IS allowed in updates }; await updateAgentHandler(mockReq, mockRes); @@ -481,13 +473,11 @@ describe('Agent Controllers - Mass Assignment Protection', () => { expect(updatedAgent.name).toBe('Updated Agent'); expect(updatedAgent.description).toBe('Updated description'); expect(updatedAgent.model).toBe('gpt-4'); - expect(updatedAgent.isCollaborative).toBe(true); expect(updatedAgent.author).toBe(existingAgentAuthorId.toString()); // Verify in database const agentInDb = await Agent.findOne({ id: existingAgentId }); expect(agentInDb.name).toBe('Updated Agent'); - expect(agentInDb.isCollaborative).toBe(true); }); test('should reject update with unauthorized fields (mass assignment protection)', async () => { @@ -542,25 +532,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { expect(updatedAgent.name).toBe('Admin Update'); }); - test('should handle projectIds updates', async () => { - mockReq.user.id = existingAgentAuthorId.toString(); - mockReq.params.id = existingAgentId; - - const projectId1 = new mongoose.Types.ObjectId().toString(); - const projectId2 = new mongoose.Types.ObjectId().toString(); - - mockReq.body = { - projectIds: [projectId1, projectId2], - }; - - await updateAgentHandler(mockReq, mockRes); - - expect(mockRes.json).toHaveBeenCalled(); - - const updatedAgent = mockRes.json.mock.calls[0][0]; - expect(updatedAgent).toBeDefined(); - }); - test('should validate tool_resources in updates', async () => { mockReq.user.id = existingAgentAuthorId.toString(); mockReq.params.id = existingAgentId; diff --git a/api/server/routes/agents/v1.js b/api/server/routes/agents/v1.js index ed989bcf44..0c7d23f8ad 100644 --- a/api/server/routes/agents/v1.js +++ b/api/server/routes/agents/v1.js @@ -21,15 +21,6 @@ const checkAgentCreate = generateCheckAccess({ getRoleByName, }); -const checkGlobalAgentShare = generateCheckAccess({ - permissionType: PermissionTypes.AGENTS, - permissions: [Permissions.USE, Permissions.CREATE], - bodyProps: { - [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], - }, - getRoleByName, -}); - router.use(requireJwtAuth); /** @@ -99,7 +90,7 @@ router.get( */ router.patch( '/:id', - checkGlobalAgentShare, + checkAgentCreate, canAccessAgentResource({ requiredPermission: PermissionBits.EDIT, resourceIdParam: 'id', @@ -148,7 +139,7 @@ router.delete( */ router.post( '/:id/revert', - checkGlobalAgentShare, + checkAgentCreate, canAccessAgentResource({ requiredPermission: PermissionBits.EDIT, resourceIdParam: 'id', diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 0adc9272bb..bf60f57e08 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -1,10 +1,9 @@ const express = require('express'); const { logger } = require('@librechat/data-schemas'); const { isEnabled, getBalanceConfig } = require('@librechat/api'); -const { Constants, CacheKeys, defaultSocialLogins } = require('librechat-data-provider'); +const { CacheKeys, defaultSocialLogins } = require('librechat-data-provider'); const { getLdapConfig } = require('~/server/services/Config/ldap'); const { getAppConfig } = require('~/server/services/Config/app'); -const { getProjectByName } = require('~/models/Project'); const { getLogStores } = require('~/cache'); const router = express.Router(); @@ -35,8 +34,6 @@ router.get('/', async function (req, res) { return today.getMonth() === 1 && today.getDate() === 11; }; - const instanceProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, '_id'); - const ldap = getLdapConfig(); try { @@ -99,7 +96,6 @@ router.get('/', async function (req, res) { sharedLinksEnabled, publicSharedLinksEnabled, analyticsGtmId: process.env.ANALYTICS_GTM_ID, - instanceProjectId: instanceProject._id.toString(), bundlerURL: process.env.SANDPACK_BUNDLER_URL, staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL, sharePointFilePickerEnabled, diff --git a/api/server/routes/prompts.js b/api/server/routes/prompts.js index 037bf04813..a0fe65ffd1 100644 --- a/api/server/routes/prompts.js +++ b/api/server/routes/prompts.js @@ -56,15 +56,6 @@ const checkPromptCreate = generateCheckAccess({ getRoleByName, }); -const checkGlobalPromptShare = generateCheckAccess({ - permissionType: PermissionTypes.PROMPTS, - permissions: [Permissions.USE, Permissions.CREATE], - bodyProps: { - [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], - }, - getRoleByName, -}); - router.use(requireJwtAuth); router.use(checkPromptAccess); @@ -364,7 +355,7 @@ const patchPromptGroup = async (req, res) => { router.patch( '/groups/:groupId', - checkGlobalPromptShare, + checkPromptCreate, canAccessPromptGroupResource({ requiredPermission: PermissionBits.EDIT, }), diff --git a/api/server/services/start/migration.js b/api/server/services/start/migration.js index 83b9c83e39..ab8d32b714 100644 --- a/api/server/services/start/migration.js +++ b/api/server/services/start/migration.js @@ -6,7 +6,6 @@ const { checkAgentPermissionsMigration, checkPromptPermissionsMigration, } = require('@librechat/api'); -const { getProjectByName } = require('~/models/Project'); const { Agent, PromptGroup } = require('~/db/models'); const { findRoleByIdentifier } = require('~/models'); @@ -20,7 +19,6 @@ async function checkMigrations() { mongoose, methods: { findRoleByIdentifier, - getProjectByName, }, AgentModel: Agent, }); @@ -33,7 +31,6 @@ async function checkMigrations() { mongoose, methods: { findRoleByIdentifier, - getProjectByName, }, PromptGroupModel: PromptGroup, }); diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx index f6e103a78d..9c4f149e57 100644 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ b/client/src/components/Prompts/Groups/ChatGroupItem.tsx @@ -16,22 +16,13 @@ import PreviewPrompt from '~/components/Prompts/PreviewPrompt'; import ListCard from '~/components/Prompts/Groups/ListCard'; import { detectVariables } from '~/utils'; -function ChatGroupItem({ - group, - instanceProjectId, -}: { - group: TPromptGroup; - instanceProjectId?: string; -}) { +function ChatGroupItem({ group }: { group: TPromptGroup }) { const localize = useLocalize(); const { submitPrompt } = useSubmitMessage(); const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); const [isVariableDialogOpen, setVariableDialogOpen] = useState(false); - const groupIsGlobal = useMemo( - () => instanceProjectId != null && group.projectIds?.includes(instanceProjectId), - [group, instanceProjectId], - ); + const groupIsGlobal = useMemo(() => group.isPublic === true, [group.isPublic]); // Check permissions for the promptGroup const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || ''); diff --git a/client/src/components/Prompts/Groups/DashGroupItem.tsx b/client/src/components/Prompts/Groups/DashGroupItem.tsx index d5e8b1a810..ee8c9acf38 100644 --- a/client/src/components/Prompts/Groups/DashGroupItem.tsx +++ b/client/src/components/Prompts/Groups/DashGroupItem.tsx @@ -19,10 +19,9 @@ import { cn } from '~/utils'; interface DashGroupItemProps { group: TPromptGroup; - instanceProjectId?: string; } -function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps) { +function DashGroupItemComponent({ group }: DashGroupItemProps) { const params = useParams(); const navigate = useNavigate(); const localize = useLocalize(); @@ -35,10 +34,7 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps const canEdit = hasPermission(PermissionBits.EDIT); const canDelete = hasPermission(PermissionBits.DELETE); - const isGlobalGroup = useMemo( - () => instanceProjectId && group.projectIds?.includes(instanceProjectId), - [group.projectIds, instanceProjectId], - ); + const isPublicGroup = useMemo(() => group.isPublic === true, [group.isPublic]); const updateGroup = useUpdatePromptGroup({ onMutate: () => { @@ -115,7 +111,7 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps

- {isGlobalGroup && ( + {isPublicGroup && ( } = useGetStartupConfig(); - const { instanceProjectId } = startupConfig; const hasCreateAccess = useHasAccess({ permissionType: PermissionTypes.PROMPTS, permission: Permissions.CREATE, @@ -73,17 +70,9 @@ export default function List({ )} {groups.map((group) => { if (isChatRoute) { - return ( - - ); + return ; } - return ( - - ); + return ; })}
diff --git a/client/src/components/SidePanel/Agents/AgentPanel.test.tsx b/client/src/components/SidePanel/Agents/AgentPanel.test.tsx index dfb45ae8d4..a3df6d52c4 100644 --- a/client/src/components/SidePanel/Agents/AgentPanel.test.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanel.test.tsx @@ -2,8 +2,8 @@ * @jest-environment jsdom */ import * as React from 'react'; -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { render, waitFor, fireEvent } from '@testing-library/react'; +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { Agent } from 'librechat-data-provider'; @@ -255,7 +255,6 @@ const mockAgentQuery = ( data: { id: 'agent-123', author: 'user-123', - isCollaborative: false, ...agent, } as Agent, isInitialLoading: false, diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx index cfceeacb33..c164cc3ad7 100644 --- a/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx @@ -26,8 +26,6 @@ mockUseWatch.mockImplementation(({ name }) => { _id: 'agent-db-123', name: 'Test Agent', author: 'user-123', - projectIds: ['project-1'], - isCollaborative: false, }; } if (name === 'id') { @@ -237,8 +235,6 @@ describe('AgentFooter', () => { _id: 'agent-db-123', name: 'Test Agent', author: 'user-123', - projectIds: ['project-1'], - isCollaborative: false, }; } if (name === 'id') { @@ -382,8 +378,6 @@ describe('AgentFooter', () => { _id: 'agent-db-123', name: 'Test Agent', author: 'different-user', // Different author - projectIds: ['project-1'], - isCollaborative: false, }; } if (name === 'id') { @@ -409,8 +403,6 @@ describe('AgentFooter', () => { _id: 'agent-db-123', name: 'Test Agent', author: 'user-123', // Same as current user - projectIds: ['project-1'], - isCollaborative: false, }; } if (name === 'id') { diff --git a/client/src/data-provider/prompts.ts b/client/src/data-provider/prompts.ts index 6c636e253f..be98e1f4a2 100644 --- a/client/src/data-provider/prompts.ts +++ b/client/src/data-provider/prompts.ts @@ -46,12 +46,7 @@ export const useUpdatePromptGroup = ( ]); const previousListData = groupListData ? structuredClone(groupListData) : undefined; - let update = variables.payload; - if (update.removeProjectIds && group?.projectIds) { - update = structuredClone(update); - update.projectIds = group.projectIds.filter((id) => !update.removeProjectIds?.includes(id)); - delete update.removeProjectIds; - } + const update = variables.payload; if (groupListData) { const newData = updateGroupFields( diff --git a/config/migrate-agent-permissions.js b/config/migrate-agent-permissions.js index b511fba50f..01d04c48ca 100644 --- a/config/migrate-agent-permissions.js +++ b/config/migrate-agent-permissions.js @@ -2,16 +2,24 @@ const path = require('path'); const { logger } = require('@librechat/data-schemas'); const { ensureRequiredCollectionsExist } = require('@librechat/api'); const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); -const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const connect = require('./connect'); const { grantPermission } = require('~/server/services/PermissionService'); -const { getProjectByName } = require('~/models/Project'); const { findRoleByIdentifier } = require('~/models'); const { Agent, AclEntry } = require('~/db/models'); +const GLOBAL_PROJECT_NAME = 'instance'; + +/** Queries the raw `projects` collection (which may still exist in the DB even though the model is removed) */ +async function getGlobalProjectAgentIds(db) { + const project = await db + .collection('projects') + .findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { agentIds: 1 } }); + return new Set(project?.agentIds || []); +} + async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 } = {}) { await connect(); @@ -24,7 +32,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 await ensureRequiredCollectionsExist(db); } - // Verify required roles exist const ownerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_OWNER); const viewerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER); const editorRole = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR); @@ -33,9 +40,7 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 throw new Error('Required roles not found. Run role seeding first.'); } - // Get global project agent IDs (stores agent.id, not agent._id) - const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']); - const globalAgentIds = new Set(globalProject?.agentIds || []); + const globalAgentIds = db ? await getGlobalProjectAgentIds(db) : new Set(); logger.info(`Found ${globalAgentIds.size} agents in global project`); @@ -52,9 +57,9 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 .lean(); const categories = { - globalEditAccess: [], // Global project + collaborative -> Public EDIT - globalViewAccess: [], // Global project + not collaborative -> Public VIEW - privateAgents: [], // Not in global project -> Private (owner only) + globalEditAccess: [], + globalViewAccess: [], + privateAgents: [], }; agentsToMigrate.forEach((agent) => { @@ -68,7 +73,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 } else { categories.privateAgents.push(agent); - // Log warning if private agent claims to be collaborative if (isCollab) { logger.warn( `Agent "${agent.name}" (${agent.id}) has isCollaborative=true but is not in global project`, @@ -130,7 +134,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 ownerGrants: 0, }; - // Process in batches for (let i = 0; i < agentsToMigrate.length; i += batchSize) { const batch = agentsToMigrate.slice(i, i + batchSize); @@ -143,7 +146,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 const isGlobal = globalAgentIds.has(agent.id); const isCollab = agent.isCollaborative; - // Always grant owner permission to author await grantPermission({ principalType: PrincipalType.USER, principalId: agent.author, @@ -154,24 +156,20 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 }); results.ownerGrants++; - // Determine public permissions for global project agents only let publicRoleId = null; let description = 'Private'; if (isGlobal) { if (isCollab) { - // Global project + collaborative = Public EDIT access publicRoleId = AccessRoleIds.AGENT_EDITOR; description = 'Global Edit'; results.publicEditGrants++; } else { - // Global project + not collaborative = Public VIEW access publicRoleId = AccessRoleIds.AGENT_VIEWER; description = 'Global View'; results.publicViewGrants++; } - // Grant public permission await grantPermission({ principalType: PrincipalType.PUBLIC, principalId: null, @@ -200,7 +198,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 } } - // Brief pause between batches await new Promise((resolve) => setTimeout(resolve, 100)); } diff --git a/config/migrate-prompt-permissions.js b/config/migrate-prompt-permissions.js index d86ee92f08..1127c44ceb 100644 --- a/config/migrate-prompt-permissions.js +++ b/config/migrate-prompt-permissions.js @@ -2,16 +2,24 @@ const path = require('path'); const { logger } = require('@librechat/data-schemas'); const { ensureRequiredCollectionsExist } = require('@librechat/api'); const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); -const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const connect = require('./connect'); const { grantPermission } = require('~/server/services/PermissionService'); -const { getProjectByName } = require('~/models/Project'); const { findRoleByIdentifier } = require('~/models'); const { PromptGroup, AclEntry } = require('~/db/models'); +const GLOBAL_PROJECT_NAME = 'instance'; + +/** Queries the raw `projects` collection (which may still exist in the DB even though the model is removed) */ +async function getGlobalProjectPromptGroupIds(db) { + const project = await db + .collection('projects') + .findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { promptGroupIds: 1 } }); + return new Set((project?.promptGroupIds || []).map((id) => id.toString())); +} + async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 } = {}) { await connect(); @@ -24,7 +32,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 await ensureRequiredCollectionsExist(db); } - // Verify required roles exist const ownerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER); const viewerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER); const editorRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR); @@ -33,11 +40,7 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 throw new Error('Required promptGroup roles not found. Run role seeding first.'); } - // Get global project prompt group IDs - const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']); - const globalPromptGroupIds = new Set( - (globalProject?.promptGroupIds || []).map((id) => id.toString()), - ); + const globalPromptGroupIds = db ? await getGlobalProjectPromptGroupIds(db) : new Set(); logger.info(`Found ${globalPromptGroupIds.size} prompt groups in global project`); @@ -54,8 +57,8 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 .lean(); const categories = { - globalViewAccess: [], // PromptGroup in global project -> Public VIEW - privateGroups: [], // Not in global project -> Private (owner only) + globalViewAccess: [], + privateGroups: [], }; promptGroupsToMigrate.forEach((group) => { @@ -115,7 +118,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 ownerGrants: 0, }; - // Process in batches for (let i = 0; i < promptGroupsToMigrate.length; i += batchSize) { const batch = promptGroupsToMigrate.slice(i, i + batchSize); @@ -127,7 +129,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 try { const isGlobalGroup = globalPromptGroupIds.has(group._id.toString()); - // Always grant owner permission to author await grantPermission({ principalType: PrincipalType.USER, principalId: group.author, @@ -138,7 +139,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 }); results.ownerGrants++; - // Grant public view permissions for promptGroups in global project if (isGlobalGroup) { await grantPermission({ principalType: PrincipalType.PUBLIC, @@ -170,7 +170,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 } } - // Brief pause between batches await new Promise((resolve) => setTimeout(resolve, 100)); } diff --git a/packages/api/src/agents/migration.ts b/packages/api/src/agents/migration.ts index 4da3852f82..0277249327 100644 --- a/packages/api/src/agents/migration.ts +++ b/packages/api/src/agents/migration.ts @@ -1,20 +1,13 @@ import { logger } from '@librechat/data-schemas'; -import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider'; +import { AccessRoleIds, ResourceType, PrincipalType } from 'librechat-data-provider'; import { ensureRequiredCollectionsExist } from '../db/utils'; import type { AccessRoleMethods, IAgent } from '@librechat/data-schemas'; import type { Model, Mongoose } from 'mongoose'; -const { GLOBAL_PROJECT_NAME } = Constants; +const GLOBAL_PROJECT_NAME = 'instance'; export interface MigrationCheckDbMethods { findRoleByIdentifier: AccessRoleMethods['findRoleByIdentifier']; - getProjectByName: ( - projectName: string, - fieldsToSelect?: string[] | null, - ) => Promise<{ - agentIds?: string[]; - [key: string]: unknown; - } | null>; } export interface MigrationCheckParams { @@ -60,7 +53,6 @@ export async function checkAgentPermissionsMigration({ await ensureRequiredCollectionsExist(db); } - // Verify required roles exist const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER); const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER); const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR); @@ -77,9 +69,13 @@ export async function checkAgentPermissionsMigration({ }; } - // Get global project agent IDs - const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']); - const globalAgentIds = new Set(globalProject?.agentIds || []); + let globalAgentIds = new Set(); + if (db) { + const project = await db + .collection('projects') + .findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { agentIds: 1 } }); + globalAgentIds = new Set(project?.agentIds || []); + } const AclEntry = mongoose.model('AclEntry'); const migratedAgentIds = await AclEntry.distinct('resourceId', { @@ -124,7 +120,6 @@ export async function checkAgentPermissionsMigration({ privateAgents: categories.privateAgents.length, }; - // Add details for debugging if (agentsToMigrate.length > 0) { result.details = { globalEditAccess: categories.globalEditAccess.map((a) => ({ @@ -152,7 +147,6 @@ export async function checkAgentPermissionsMigration({ return result; } catch (error) { logger.error('Failed to check agent permissions migration', error); - // Return zero counts on error to avoid blocking startup return { totalToMigrate: 0, globalEditAccess: 0, @@ -170,7 +164,6 @@ export function logAgentMigrationWarning(result: MigrationCheckResult): void { return; } - // Create a visible warning box const border = '='.repeat(80); const warning = [ '', @@ -201,10 +194,8 @@ export function logAgentMigrationWarning(result: MigrationCheckResult): void { '', ]; - // Use console methods directly for visibility console.log('\n' + warning.join('\n') + '\n'); - // Also log with logger for consistency logger.warn('Agent permissions migration required', { totalToMigrate: result.totalToMigrate, globalEditAccess: result.globalEditAccess, diff --git a/packages/api/src/agents/validation.ts b/packages/api/src/agents/validation.ts index d427b3639e..8119c97204 100644 --- a/packages/api/src/agents/validation.ts +++ b/packages/api/src/agents/validation.ts @@ -94,9 +94,6 @@ export const agentUpdateSchema = agentBaseSchema.extend({ avatar: z.union([agentAvatarSchema, z.null()]).optional(), provider: z.string().optional(), model: z.string().nullable().optional(), - projectIds: z.array(z.string()).optional(), - removeProjectIds: z.array(z.string()).optional(), - isCollaborative: z.boolean().optional(), }); interface ValidateAgentModelParams { diff --git a/packages/api/src/middleware/access.spec.ts b/packages/api/src/middleware/access.spec.ts index d7ca690c48..c0efa9fcc1 100644 --- a/packages/api/src/middleware/access.spec.ts +++ b/packages/api/src/middleware/access.spec.ts @@ -216,17 +216,12 @@ describe('access middleware', () => { defaultParams.getRoleByName.mockResolvedValue(mockRole); - const checkObject = { - projectIds: ['project1'], - removeProjectIds: ['project2'], - }; + const checkObject = {}; const result = await checkAccess({ ...defaultParams, permissions: [Permissions.USE, Permissions.SHARE], - bodyProps: { - [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], - } as Record, + bodyProps: {} as Record, checkObject, }); expect(result).toBe(true); @@ -244,17 +239,12 @@ describe('access middleware', () => { defaultParams.getRoleByName.mockResolvedValue(mockRole); - const checkObject = { - projectIds: ['project1'], - // missing removeProjectIds - }; + const checkObject = {}; const result = await checkAccess({ ...defaultParams, permissions: [Permissions.SHARE], - bodyProps: { - [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], - } as Record, + bodyProps: {} as Record, checkObject, }); expect(result).toBe(false); @@ -343,17 +333,12 @@ describe('access middleware', () => { } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); - mockReq.body = { - projectIds: ['project1'], - removeProjectIds: ['project2'], - }; + mockReq.body = {}; const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARE], - bodyProps: { - [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], - } as Record, + bodyProps: {} as Record, getRoleByName: mockGetRoleByName, }); diff --git a/packages/api/src/prompts/migration.ts b/packages/api/src/prompts/migration.ts index a9e71d427a..be2b32e26d 100644 --- a/packages/api/src/prompts/migration.ts +++ b/packages/api/src/prompts/migration.ts @@ -1,20 +1,13 @@ import { logger } from '@librechat/data-schemas'; -import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider'; +import { AccessRoleIds, ResourceType, PrincipalType } from 'librechat-data-provider'; import { ensureRequiredCollectionsExist } from '../db/utils'; import type { AccessRoleMethods, IPromptGroupDocument } from '@librechat/data-schemas'; import type { Model, Mongoose } from 'mongoose'; -const { GLOBAL_PROJECT_NAME } = Constants; +const GLOBAL_PROJECT_NAME = 'instance'; export interface PromptMigrationCheckDbMethods { findRoleByIdentifier: AccessRoleMethods['findRoleByIdentifier']; - getProjectByName: ( - projectName: string, - fieldsToSelect?: string[] | null, - ) => Promise<{ - promptGroupIds?: string[]; - [key: string]: unknown; - } | null>; } export interface PromptMigrationCheckParams { @@ -53,13 +46,11 @@ export async function checkPromptPermissionsMigration({ logger.debug('Checking if prompt permissions migration is needed'); try { - /** Native MongoDB database instance */ const db = mongoose.connection.db; if (db) { await ensureRequiredCollectionsExist(db); } - // Verify required roles exist const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER); const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER); const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR); @@ -75,11 +66,15 @@ export async function checkPromptPermissionsMigration({ }; } - /** Global project prompt group IDs */ - const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']); - const globalPromptGroupIds = new Set( - (globalProject?.promptGroupIds || []).map((id) => id.toString()), - ); + let globalPromptGroupIds = new Set(); + if (db) { + const project = await db + .collection('projects') + .findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { promptGroupIds: 1 } }); + globalPromptGroupIds = new Set( + (project?.promptGroupIds || []).map((id: { toString(): string }) => id.toString()), + ); + } const AclEntry = mongoose.model('AclEntry'); const migratedGroupIds = await AclEntry.distinct('resourceId', { @@ -118,7 +113,6 @@ export async function checkPromptPermissionsMigration({ privateGroups: categories.privateGroups.length, }; - // Add details for debugging if (promptGroupsToMigrate.length > 0) { result.details = { globalViewAccess: categories.globalViewAccess.map((g) => ({ @@ -143,7 +137,6 @@ export async function checkPromptPermissionsMigration({ return result; } catch (error) { logger.error('Failed to check prompt permissions migration', error); - // Return zero counts on error to avoid blocking startup return { totalToMigrate: 0, globalViewAccess: 0, @@ -160,7 +153,6 @@ export function logPromptMigrationWarning(result: PromptMigrationCheckResult): v return; } - // Create a visible warning box const border = '='.repeat(80); const warning = [ '', @@ -190,10 +182,8 @@ export function logPromptMigrationWarning(result: PromptMigrationCheckResult): v '', ]; - // Use console methods directly for visibility console.log('\n' + warning.join('\n') + '\n'); - // Also log with logger for consistency logger.warn('Prompt permissions migration required', { totalToMigrate: result.totalToMigrate, globalViewAccess: result.globalViewAccess, diff --git a/packages/api/src/prompts/schemas.spec.ts b/packages/api/src/prompts/schemas.spec.ts index 0008e31b51..2ba34e17f2 100644 --- a/packages/api/src/prompts/schemas.spec.ts +++ b/packages/api/src/prompts/schemas.spec.ts @@ -30,26 +30,6 @@ describe('updatePromptGroupSchema', () => { } }); - it('should accept valid projectIds array', () => { - const result = updatePromptGroupSchema.safeParse({ - projectIds: ['proj1', 'proj2'], - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.projectIds).toEqual(['proj1', 'proj2']); - } - }); - - it('should accept valid removeProjectIds array', () => { - const result = updatePromptGroupSchema.safeParse({ - removeProjectIds: ['proj1'], - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.removeProjectIds).toEqual(['proj1']); - } - }); - it('should accept valid command field', () => { const result = updatePromptGroupSchema.safeParse({ command: 'my-command-123' }); expect(result.success).toBe(true); diff --git a/packages/api/src/prompts/schemas.ts b/packages/api/src/prompts/schemas.ts index 628c07d954..43cb9d7e94 100644 --- a/packages/api/src/prompts/schemas.ts +++ b/packages/api/src/prompts/schemas.ts @@ -14,10 +14,6 @@ export const updatePromptGroupSchema = z oneliner: z.string().max(500).optional(), /** Category for organizing prompt groups */ category: z.string().max(100).optional(), - /** Project IDs to add for sharing */ - projectIds: z.array(z.string()).optional(), - /** Project IDs to remove from sharing */ - removeProjectIds: z.array(z.string()).optional(), /** Command shortcut for the prompt group */ command: z .string() diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 043e1952f3..35411a1c9c 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -781,7 +781,6 @@ export type TStartupConfig = { sharedLinksEnabled: boolean; publicSharedLinksEnabled: boolean; analyticsGtmId?: string; - instanceProjectId: string; bundlerURL?: string; staticBundlerURL?: string; sharePointFilePickerEnabled?: boolean; @@ -1771,8 +1770,6 @@ export enum Constants { SAVED_TAG = 'Saved', /** Max number of Conversation starters for Agents/Assistants */ MAX_CONVO_STARTERS = 4, - /** Global/instance Project Name */ - GLOBAL_PROJECT_NAME = 'instance', /** Delimiter for MCP tools */ mcp_delimiter = '_mcp_', /** Prefix for MCP plugins */ diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 63a7ed574e..7eb0482e9f 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -258,11 +258,8 @@ export const defaultAgentFormValues = { tools: [], tool_options: {}, provider: {}, - projectIds: [], edges: [], artifacts: '', - /** @deprecated Use ACL permissions instead */ - isCollaborative: false, recursion_limit: undefined, [Tools.execute_code]: false, [Tools.file_search]: false, diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 5895fba321..3716f67b05 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -534,7 +534,6 @@ export type TPromptGroup = { command?: string; oneliner?: string; category?: string; - projectIds?: string[]; productionId?: string | null; productionPrompt?: Pick | null; author: string; @@ -587,9 +586,7 @@ export type TCreatePromptResponse = { group?: TPromptGroup; }; -export type TUpdatePromptGroupPayload = Partial & { - removeProjectIds?: string[]; -}; +export type TUpdatePromptGroupPayload = Partial; export type TUpdatePromptGroupVariables = { id: string; diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 8865767240..22072403d3 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -252,15 +252,12 @@ export type Agent = { instructions?: string | null; additional_instructions?: string | null; tools?: string[]; - projectIds?: string[]; tool_kwargs?: Record; metadata?: Record; provider: AgentProvider; model: string | null; model_parameters: AgentModelParameters; conversation_starters?: string[]; - /** @deprecated Use ACL permissions instead */ - isCollaborative?: boolean; tool_resources?: AgentToolResources; /** @deprecated Use edges instead */ agent_ids?: string[]; @@ -313,9 +310,6 @@ export type AgentUpdateParams = { provider?: AgentProvider; model?: string | null; model_parameters?: AgentModelParameters; - projectIds?: string[]; - removeProjectIds?: string[]; - isCollaborative?: boolean; } & Pick< Agent, | 'agent_ids' diff --git a/packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts index f2561137b7..3c4ce62337 100644 --- a/packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts +++ b/packages/data-schemas/misc/ferretdb/migrationAntiJoin.ferretdb.spec.ts @@ -24,7 +24,6 @@ const agentSchema = new Schema({ id: { type: String, required: true }, name: { type: String, required: true }, author: { type: String }, - isCollaborative: { type: Boolean, default: false }, }); const promptGroupSchema = new Schema({ @@ -107,7 +106,7 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () = _id: { $nin: migratedIds }, author: { $exists: true, $ne: null }, }) - .select('_id id name author isCollaborative') + .select('_id id name author') .lean(); expect(toMigrate).toHaveLength(2); @@ -197,7 +196,6 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () = id: 'proj_agent', name: 'Field Test', author: 'user1', - isCollaborative: true, }); const migratedIds = await AclEntry.distinct('resourceId', { @@ -209,7 +207,7 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () = _id: { $nin: migratedIds }, author: { $exists: true, $ne: null }, }) - .select('_id id name author isCollaborative') + .select('_id id name author') .lean(); expect(toMigrate).toHaveLength(1); @@ -218,7 +216,6 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () = expect(agent).toHaveProperty('id', 'proj_agent'); expect(agent).toHaveProperty('name', 'Field Test'); expect(agent).toHaveProperty('author', 'user1'); - expect(agent).toHaveProperty('isCollaborative', true); }); }); diff --git a/packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts index 7e6c8ad1b0..255dfeda8f 100644 --- a/packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts +++ b/packages/data-schemas/misc/ferretdb/promptLookup.ferretdb.spec.ts @@ -27,7 +27,6 @@ const promptGroupSchema = new Schema( author: { type: Schema.Types.ObjectId, required: true, index: true }, authorName: { type: String, required: true }, command: { type: String }, - projectIds: { type: [Schema.Types.ObjectId], default: [] }, }, { timestamps: true }, ); @@ -51,7 +50,6 @@ type PromptGroupDoc = mongoose.Document & { oneliner: string; numberOfGenerations: number; command?: string; - projectIds: Types.ObjectId[]; createdAt: Date; updatedAt: Date; }; @@ -226,7 +224,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () => .skip(skip) .limit(limit) .select( - 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', ) .lean(), PromptGroup.countDocuments(query), @@ -273,7 +271,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () => .sort({ updatedAt: -1, _id: 1 }) .limit(normalizedLimit + 1) .select( - 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', ) .lean(); @@ -303,7 +301,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () => const groups = await PromptGroup.find({ _id: { $in: accessibleIds } }) .sort({ updatedAt: -1, _id: 1 }) .select( - 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', ) .lean(); @@ -326,7 +324,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () => const groups = await PromptGroup.find({}) .select( - 'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', ) .lean(); const result = await attachProductionPrompts( @@ -339,7 +337,6 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () => expect(item.numberOfGenerations).toBe(5); expect(item.oneliner).toBe('A test prompt'); expect(item.category).toBe('testing'); - expect(item.projectIds).toEqual([]); expect(item.productionId).toBeDefined(); expect(item.author).toBeDefined(); expect(item.authorName).toBe('Test User'); diff --git a/packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts b/packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts index 446cb701d1..0e2e273609 100644 --- a/packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts +++ b/packages/data-schemas/misc/ferretdb/pullAll.ferretdb.spec.ts @@ -37,7 +37,6 @@ const projectSchema = new Schema({ const agentSchema = new Schema({ name: { type: String, required: true }, - projectIds: { type: [String], default: [] }, tool_resources: { type: Schema.Types.Mixed, default: {} }, }); @@ -197,23 +196,6 @@ describeIfFerretDB('$pullAll FerretDB compatibility', () => { expect(doc.agentIds).toEqual(['a2', 'a4']); }); - it('should remove projectIds from an agent', async () => { - await Agent.create({ - name: 'Test Agent', - projectIds: ['p1', 'p2', 'p3'], - }); - - await Agent.findOneAndUpdate( - { name: 'Test Agent' }, - { $pullAll: { projectIds: ['p1', 'p3'] } }, - { new: true }, - ); - - const updated = await Agent.findOne({ name: 'Test Agent' }).lean(); - const doc = updated as Record; - expect(doc.projectIds).toEqual(['p2']); - }); - it('should handle removing from nested dynamic paths (tool_resources)', async () => { await Agent.create({ name: 'Resource Agent', diff --git a/packages/data-schemas/src/models/index.ts b/packages/data-schemas/src/models/index.ts index ca1a6259be..068aba69ed 100644 --- a/packages/data-schemas/src/models/index.ts +++ b/packages/data-schemas/src/models/index.ts @@ -13,7 +13,6 @@ import { createActionModel } from './action'; import { createAssistantModel } from './assistant'; import { createFileModel } from './file'; import { createBannerModel } from './banner'; -import { createProjectModel } from './project'; import { createKeyModel } from './key'; import { createPluginAuthModel } from './pluginAuth'; import { createTransactionModel } from './transaction'; @@ -48,7 +47,6 @@ export function createModels(mongoose: typeof import('mongoose')) { Assistant: createAssistantModel(mongoose), File: createFileModel(mongoose), Banner: createBannerModel(mongoose), - Project: createProjectModel(mongoose), Key: createKeyModel(mongoose), PluginAuth: createPluginAuthModel(mongoose), Transaction: createTransactionModel(mongoose), diff --git a/packages/data-schemas/src/models/project.ts b/packages/data-schemas/src/models/project.ts deleted file mode 100644 index c68f532bc3..0000000000 --- a/packages/data-schemas/src/models/project.ts +++ /dev/null @@ -1,8 +0,0 @@ -import projectSchema, { IMongoProject } from '~/schema/project'; - -/** - * Creates or returns the Project model using the provided mongoose instance and schema - */ -export function createProjectModel(mongoose: typeof import('mongoose')) { - return mongoose.models.Project || mongoose.model('Project', projectSchema); -} diff --git a/packages/data-schemas/src/schema/agent.ts b/packages/data-schemas/src/schema/agent.ts index 32bba8bef8..eff4b8e675 100644 --- a/packages/data-schemas/src/schema/agent.ts +++ b/packages/data-schemas/src/schema/agent.ts @@ -76,10 +76,6 @@ const agentSchema = new Schema( type: [{ type: Schema.Types.Mixed }], default: [], }, - isCollaborative: { - type: Boolean, - default: undefined, - }, conversation_starters: { type: [String], default: [], @@ -88,11 +84,6 @@ const agentSchema = new Schema( type: Schema.Types.Mixed, default: {}, }, - projectIds: { - type: [Schema.Types.ObjectId], - ref: 'Project', - index: true, - }, versions: { type: [Schema.Types.Mixed], default: [], diff --git a/packages/data-schemas/src/schema/index.ts b/packages/data-schemas/src/schema/index.ts index 454780b8bf..2a58f7c3cc 100644 --- a/packages/data-schemas/src/schema/index.ts +++ b/packages/data-schemas/src/schema/index.ts @@ -13,7 +13,6 @@ export { default as keySchema } from './key'; export { default as messageSchema } from './message'; export { default as pluginAuthSchema } from './pluginAuth'; export { default as presetSchema } from './preset'; -export { default as projectSchema } from './project'; export { default as promptSchema } from './prompt'; export { default as promptGroupSchema } from './promptGroup'; export { default as roleSchema } from './role'; diff --git a/packages/data-schemas/src/schema/project.ts b/packages/data-schemas/src/schema/project.ts deleted file mode 100644 index 05c2ddc6f2..0000000000 --- a/packages/data-schemas/src/schema/project.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Schema, Document, Types } from 'mongoose'; - -export interface IMongoProject extends Document { - name: string; - promptGroupIds: Types.ObjectId[]; - agentIds: string[]; - createdAt?: Date; - updatedAt?: Date; -} - -const projectSchema = new Schema( - { - name: { - type: String, - required: true, - index: true, - }, - promptGroupIds: { - type: [Schema.Types.ObjectId], - ref: 'PromptGroup', - default: [], - }, - agentIds: { - type: [String], - ref: 'Agent', - default: [], - }, - }, - { - timestamps: true, - }, -); - -export default projectSchema; diff --git a/packages/data-schemas/src/schema/promptGroup.ts b/packages/data-schemas/src/schema/promptGroup.ts index 2e8bb4ef82..d751c67557 100644 --- a/packages/data-schemas/src/schema/promptGroup.ts +++ b/packages/data-schemas/src/schema/promptGroup.ts @@ -22,12 +22,6 @@ const promptGroupSchema = new Schema( default: '', index: true, }, - projectIds: { - type: [Schema.Types.ObjectId], - ref: 'Project', - index: true, - default: [], - }, productionId: { type: Schema.Types.ObjectId, ref: 'Prompt', diff --git a/packages/data-schemas/src/types/agent.ts b/packages/data-schemas/src/types/agent.ts index 3549e88b4a..f163ab63bd 100644 --- a/packages/data-schemas/src/types/agent.ts +++ b/packages/data-schemas/src/types/agent.ts @@ -31,11 +31,8 @@ export interface IAgent extends Omit { /** @deprecated Use edges instead */ agent_ids?: string[]; edges?: GraphEdge[]; - /** @deprecated Use ACL permissions instead */ - isCollaborative?: boolean; conversation_starters?: string[]; tool_resources?: unknown; - projectIds?: Types.ObjectId[]; versions?: Omit[]; category: string; support_contact?: ISupportContact; diff --git a/packages/data-schemas/src/types/prompts.ts b/packages/data-schemas/src/types/prompts.ts index d99d36eb73..53f09dcd49 100644 --- a/packages/data-schemas/src/types/prompts.ts +++ b/packages/data-schemas/src/types/prompts.ts @@ -14,7 +14,6 @@ export interface IPromptGroup { numberOfGenerations: number; oneliner: string; category: string; - projectIds: Types.ObjectId[]; productionId: Types.ObjectId; author: Types.ObjectId; authorName: string; From 8ba2bde5c15cdd6f518ff25584127dbde75a57a6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 17 Feb 2026 18:23:44 -0500 Subject: [PATCH 095/111] =?UTF-8?q?=F0=9F=93=A6=20refactor:=20Consolidate?= =?UTF-8?q?=20DB=20models,=20encapsulating=20Mongoose=20usage=20in=20`data?= =?UTF-8?q?-schemas`=20(#11830)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`. --- api/app/clients/BaseClient.js | 61 +- .../tools/structured/GeminiImageGen.js | 3 +- api/app/clients/tools/util/handleTools.js | 2 +- api/models/Action.js | 73 - api/models/Agent.js | 890 ----------- api/models/Assistant.js | 62 - api/models/Banner.js | 28 - api/models/Categories.js | 57 - api/models/Conversation.js | 373 ----- api/models/ConversationTag.js | 284 ---- api/models/File.js | 250 --- api/models/File.spec.js | 736 --------- api/models/Message.js | 372 ----- api/models/Preset.js | 82 - api/models/Prompt.js | 588 ------- api/models/Prompt.spec.js | 784 ---------- api/models/Role.js | 304 ---- api/models/ToolCall.js | 96 -- api/models/Transaction.js | 223 --- api/models/balanceMethods.js | 156 -- api/models/index.js | 43 +- api/models/interface.js | 24 - api/models/inviteUser.js | 68 - api/models/loadAddedAgent.js | 218 --- api/models/spendTokens.js | 140 -- api/models/userMethods.js | 31 - api/server/controllers/Balance.js | 22 +- .../controllers/PermissionsController.js | 17 +- api/server/controllers/UserController.js | 99 +- api/server/controllers/UserController.spec.js | 60 +- .../agents/__tests__/openai.spec.js | 25 +- .../agents/__tests__/responses.unit.spec.js | 29 +- .../controllers/agents/__tests__/v1.spec.js | 6 +- api/server/controllers/agents/client.js | 20 +- api/server/controllers/agents/client.test.js | 13 +- api/server/controllers/agents/errors.js | 2 +- api/server/controllers/agents/openai.js | 15 +- .../agents/recordCollectedUsage.spec.js | 2 +- api/server/controllers/agents/request.js | 40 +- api/server/controllers/agents/responses.js | 27 +- api/server/controllers/agents/v1.js | 54 +- api/server/controllers/agents/v1.spec.js | 24 +- api/server/controllers/assistants/chatV1.js | 41 +- api/server/controllers/assistants/chatV2.js | 41 +- api/server/controllers/assistants/errors.js | 2 +- api/server/controllers/assistants/v1.js | 3 +- api/server/controllers/assistants/v2.js | 2 +- api/server/controllers/tools.js | 4 +- api/server/experimental.js | 6 +- api/server/index.js | 6 +- api/server/middleware/abortMiddleware.js | 8 +- api/server/middleware/abortMiddleware.spec.js | 8 - api/server/middleware/abortRun.js | 3 +- .../accessResources/canAccessAgentFromBody.js | 5 +- .../accessResources/canAccessAgentResource.js | 2 +- .../canAccessAgentResource.spec.js | 4 +- .../canAccessPromptGroupResource.js | 2 +- .../canAccessPromptViaGroup.js | 2 +- .../middleware/accessResources/fileAccess.js | 3 +- .../accessResources/fileAccess.spec.js | 3 +- .../middleware/assistants/validateAuthor.js | 2 +- api/server/middleware/checkInviteUser.js | 7 +- .../middleware/checkPeoplePickerAccess.js | 2 +- .../checkPeoplePickerAccess.spec.js | 4 +- .../middleware/checkSharePublicAccess.js | 2 +- .../middleware/checkSharePublicAccess.spec.js | 4 +- api/server/middleware/denyRequest.js | 6 +- api/server/middleware/error.js | 9 +- api/server/middleware/roles/access.spec.js | 2 +- api/server/middleware/validate/convoAccess.js | 2 +- api/server/routes/__tests__/convos.spec.js | 16 +- api/server/routes/accessPermissions.test.js | 2 +- api/server/routes/admin/auth.js | 5 +- api/server/routes/agents/actions.js | 26 +- api/server/routes/agents/chat.js | 2 +- api/server/routes/agents/index.js | 14 +- api/server/routes/agents/openai.js | 12 +- api/server/routes/agents/responses.js | 12 +- api/server/routes/agents/v1.js | 2 +- api/server/routes/apiKeys.js | 2 +- api/server/routes/assistants/actions.js | 17 +- api/server/routes/auth.js | 5 +- api/server/routes/banner.js | 6 +- api/server/routes/categories.js | 2 +- api/server/routes/convos.js | 36 +- api/server/routes/files/files.agents.test.js | 3 +- api/server/routes/files/files.js | 20 +- api/server/routes/files/files.test.js | 3 +- api/server/routes/mcp.js | 24 +- api/server/routes/memories.js | 2 +- api/server/routes/messages.js | 101 +- api/server/routes/oauth.js | 5 +- api/server/routes/prompts.js | 4 +- api/server/routes/prompts.test.js | 21 +- api/server/routes/roles.js | 2 +- api/server/routes/tags.js | 4 +- api/server/services/ActionService.js | 11 +- .../services/Endpoints/agents/addedConvo.js | 17 +- api/server/services/Endpoints/agents/build.js | 6 +- .../services/Endpoints/agents/initialize.js | 8 +- api/server/services/Endpoints/agents/title.js | 6 +- .../services/Endpoints/assistants/build.js | 2 +- .../services/Endpoints/assistants/title.js | 15 +- .../Endpoints/azureAssistants/build.js | 2 +- .../services/Files/Audio/streamAudio.js | 2 +- .../services/Files/Audio/streamAudio.spec.js | 2 +- api/server/services/Files/Citations/index.js | 5 +- api/server/services/Files/permissions.js | 2 +- api/server/services/Files/process.js | 35 +- api/server/services/PermissionService.js | 41 +- api/server/services/Threads/manage.js | 49 +- api/server/services/cleanup.js | 2 +- api/server/services/start/migration.js | 5 +- api/server/utils/import/fork.js | 3 +- api/server/utils/import/fork.spec.js | 18 +- api/server/utils/import/importBatchBuilder.js | 4 +- .../utils/import/importers-timestamp.spec.js | 4 +- api/server/utils/import/importers.spec.js | 7 +- api/strategies/localStrategy.js | 7 +- .../Files/processFileCitations.test.js | 23 +- .../migrate-prompt-permissions.spec.js | 4 +- config/add-balance.js | 2 +- eslint.config.mjs | 11 + .../api/src/agents/__tests__/load.spec.ts | 397 +++++ packages/api/src/agents/added.ts | 230 +++ packages/api/src/agents/index.ts | 2 + packages/api/src/agents/load.ts | 162 ++ packages/api/src/apiKeys/permissions.ts | 61 +- packages/api/src/auth/index.ts | 2 + packages/api/src/auth/invite.ts | 61 + packages/api/src/auth/password.ts | 25 + .../src/mcp/registry/db/ServerConfigsDB.ts | 10 +- packages/api/src/middleware/access.spec.ts | 8 +- packages/api/src/middleware/balance.spec.ts | 84 +- packages/api/src/middleware/balance.ts | 24 +- packages/api/src/middleware/checkBalance.ts | 168 ++ packages/api/src/middleware/index.ts | 1 + packages/api/src/prompts/format.ts | 2 +- packages/api/src/utils/common.ts | 9 - packages/api/src/utils/index.ts | 1 - packages/data-schemas/rollup.config.js | 2 +- packages/data-schemas/src/index.ts | 10 +- packages/data-schemas/src/methods/aclEntry.ts | 112 +- packages/data-schemas/src/methods/action.ts | 77 + .../data-schemas/src/methods/agent.spec.ts | 1359 ++++++----------- packages/data-schemas/src/methods/agent.ts | 762 +++++++++ .../data-schemas/src/methods/assistant.ts | 69 + packages/data-schemas/src/methods/banner.ts | 33 + .../data-schemas/src/methods/categories.ts | 33 + .../src/methods/conversation.spec.ts | 389 ++--- .../data-schemas/src/methods/conversation.ts | 488 ++++++ .../methods/conversationTag.methods.spec.ts | 45 +- .../src/methods/conversationTag.ts | 312 ++++ .../src/methods/convoStructure.spec.ts | 116 +- .../data-schemas/src/methods/file.acl.spec.ts | 405 +++++ packages/data-schemas/src/methods/index.ts | 156 +- packages/data-schemas/src/methods/memory.ts | 16 + .../data-schemas/src/methods/message.spec.ts | 426 +++--- packages/data-schemas/src/methods/message.ts | 399 +++++ packages/data-schemas/src/methods/preset.ts | 132 ++ .../data-schemas/src/methods/prompt.spec.ts | 627 ++++++++ packages/data-schemas/src/methods/prompt.ts | 691 +++++++++ .../src/methods/role.methods.spec.ts | 78 +- packages/data-schemas/src/methods/role.ts | 319 +++- .../src/methods/spendTokens.spec.ts | 165 +- .../data-schemas/src/methods/spendTokens.ts | 145 ++ .../data-schemas/src/methods/test-helpers.ts | 38 + packages/data-schemas/src/methods/toolCall.ts | 97 ++ .../src/methods/transaction.spec.ts | 159 +- .../data-schemas/src/methods/transaction.ts | 359 ++++- .../data-schemas/src/methods/tx.spec.ts | 155 +- .../data-schemas/src/methods/tx.ts | 373 +++-- .../data-schemas/src/methods/userGroup.ts | 59 + packages/data-schemas/src/types/agent.ts | 4 +- packages/data-schemas/src/types/balance.ts | 11 + packages/data-schemas/src/types/message.ts | 4 +- packages/data-schemas/src/utils/index.ts | 2 + packages/data-schemas/src/utils/string.ts | 6 + .../src/utils/tempChatRetention.spec.ts | 2 +- .../src/utils/tempChatRetention.ts | 4 +- packages/data-schemas/tsconfig.build.json | 10 + packages/data-schemas/tsconfig.json | 7 +- 182 files changed, 8675 insertions(+), 8457 deletions(-) delete mode 100644 api/models/Action.js delete mode 100644 api/models/Agent.js delete mode 100644 api/models/Assistant.js delete mode 100644 api/models/Banner.js delete mode 100644 api/models/Categories.js delete mode 100644 api/models/Conversation.js delete mode 100644 api/models/ConversationTag.js delete mode 100644 api/models/File.js delete mode 100644 api/models/File.spec.js delete mode 100644 api/models/Message.js delete mode 100644 api/models/Preset.js delete mode 100644 api/models/Prompt.js delete mode 100644 api/models/Prompt.spec.js delete mode 100644 api/models/Role.js delete mode 100644 api/models/ToolCall.js delete mode 100644 api/models/Transaction.js delete mode 100644 api/models/balanceMethods.js delete mode 100644 api/models/interface.js delete mode 100644 api/models/inviteUser.js delete mode 100644 api/models/loadAddedAgent.js delete mode 100644 api/models/spendTokens.js delete mode 100644 api/models/userMethods.js rename api/models/PromptGroupMigration.spec.js => config/__tests__/migrate-prompt-permissions.spec.js (98%) create mode 100644 packages/api/src/agents/__tests__/load.spec.ts create mode 100644 packages/api/src/agents/added.ts create mode 100644 packages/api/src/agents/load.ts create mode 100644 packages/api/src/auth/invite.ts create mode 100644 packages/api/src/auth/password.ts create mode 100644 packages/api/src/middleware/checkBalance.ts create mode 100644 packages/data-schemas/src/methods/action.ts rename api/models/Agent.spec.js => packages/data-schemas/src/methods/agent.spec.ts (71%) create mode 100644 packages/data-schemas/src/methods/agent.ts create mode 100644 packages/data-schemas/src/methods/assistant.ts create mode 100644 packages/data-schemas/src/methods/banner.ts create mode 100644 packages/data-schemas/src/methods/categories.ts rename api/models/Conversation.spec.js => packages/data-schemas/src/methods/conversation.spec.ts (67%) create mode 100644 packages/data-schemas/src/methods/conversation.ts rename api/models/ConversationTag.spec.js => packages/data-schemas/src/methods/conversationTag.methods.spec.ts (73%) create mode 100644 packages/data-schemas/src/methods/conversationTag.ts rename api/models/convoStructure.spec.js => packages/data-schemas/src/methods/convoStructure.spec.ts (69%) create mode 100644 packages/data-schemas/src/methods/file.acl.spec.ts rename api/models/Message.spec.js => packages/data-schemas/src/methods/message.spec.ts (67%) create mode 100644 packages/data-schemas/src/methods/message.ts create mode 100644 packages/data-schemas/src/methods/preset.ts create mode 100644 packages/data-schemas/src/methods/prompt.spec.ts create mode 100644 packages/data-schemas/src/methods/prompt.ts rename api/models/Role.spec.js => packages/data-schemas/src/methods/role.methods.spec.ts (87%) rename api/models/spendTokens.spec.js => packages/data-schemas/src/methods/spendTokens.spec.ts (87%) create mode 100644 packages/data-schemas/src/methods/spendTokens.ts create mode 100644 packages/data-schemas/src/methods/test-helpers.ts create mode 100644 packages/data-schemas/src/methods/toolCall.ts rename api/models/Transaction.spec.js => packages/data-schemas/src/methods/transaction.spec.ts (89%) rename api/models/tx.spec.js => packages/data-schemas/src/methods/tx.spec.ts (95%) rename api/models/tx.js => packages/data-schemas/src/methods/tx.ts (63%) create mode 100644 packages/data-schemas/src/utils/string.ts rename packages/{api => data-schemas}/src/utils/tempChatRetention.spec.ts (98%) rename packages/{api => data-schemas}/src/utils/tempChatRetention.ts (95%) create mode 100644 packages/data-schemas/tsconfig.build.json diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 8f931f8a5e..a7ad089d20 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -3,6 +3,7 @@ const fetch = require('node-fetch'); const { logger } = require('@librechat/data-schemas'); const { countTokens, + checkBalance, getBalanceConfig, buildMessageFiles, extractFileContext, @@ -23,18 +24,11 @@ const { supportsBalanceCheck, isBedrockDocumentType, } = require('librechat-data-provider'); -const { - updateMessage, - getMessages, - saveMessage, - saveConvo, - getConvo, - getFiles, -} = require('~/models'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { checkBalance } = require('~/models/balanceMethods'); const { truncateToolCallOutputs } = require('./prompts'); +const { logViolation } = require('~/cache'); const TextStream = require('./TextStream'); +const db = require('~/models'); class BaseClient { constructor(apiKey, options = {}) { @@ -700,18 +694,26 @@ class BaseClient { balanceConfig?.enabled && supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint] ) { - await checkBalance({ - req: this.options.req, - res: this.options.res, - txData: { - user: this.user, - tokenType: 'prompt', - amount: promptTokens, - endpoint: this.options.endpoint, - model: this.modelOptions?.model ?? this.model, - endpointTokenConfig: this.options.endpointTokenConfig, + await checkBalance( + { + req: this.options.req, + res: this.options.res, + txData: { + user: this.user, + tokenType: 'prompt', + amount: promptTokens, + endpoint: this.options.endpoint, + model: this.modelOptions?.model ?? this.model, + endpointTokenConfig: this.options.endpointTokenConfig, + }, }, - }); + { + logViolation, + getMultiplier: db.getMultiplier, + findBalanceByUser: db.findBalanceByUser, + createAutoRefillTransaction: db.createAutoRefillTransaction, + }, + ); } const { completion, metadata } = await this.sendCompletion(payload, opts); @@ -909,7 +911,7 @@ class BaseClient { async loadHistory(conversationId, parentMessageId = null) { logger.debug('[BaseClient] Loading history:', { conversationId, parentMessageId }); - const messages = (await getMessages({ conversationId })) ?? []; + const messages = (await db.getMessages({ conversationId })) ?? []; if (messages.length === 0) { return []; @@ -965,8 +967,13 @@ class BaseClient { } const hasAddedConvo = this.options?.req?.body?.addedConvo != null; - const savedMessage = await saveMessage( - this.options?.req, + const reqCtx = { + userId: this.options?.req?.user?.id, + isTemporary: this.options?.req?.body?.isTemporary, + interfaceConfig: this.options?.req?.config?.interfaceConfig, + }; + const savedMessage = await db.saveMessage( + reqCtx, { ...message, endpoint: this.options.endpoint, @@ -991,7 +998,7 @@ class BaseClient { const existingConvo = this.fetchedConvo === true ? null - : await getConvo(this.options?.req?.user?.id, message.conversationId); + : await db.getConvo(this.options?.req?.user?.id, message.conversationId); const unsetFields = {}; const exceptions = new Set(['spec', 'iconURL']); @@ -1018,7 +1025,7 @@ class BaseClient { } } - const conversation = await saveConvo(this.options?.req, fieldsToKeep, { + const conversation = await db.saveConvo(reqCtx, fieldsToKeep, { context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveConvo', unsetFields, }); @@ -1031,7 +1038,7 @@ class BaseClient { * @param {Partial} message */ async updateMessageInDatabase(message) { - await updateMessage(this.options.req, message); + await db.updateMessage(this.options?.req?.user?.id, message); } /** @@ -1431,7 +1438,7 @@ class BaseClient { return message; } - const files = await getFiles( + const files = await db.getFiles( { file_id: { $in: fileIds }, }, diff --git a/api/app/clients/tools/structured/GeminiImageGen.js b/api/app/clients/tools/structured/GeminiImageGen.js index 0bd1e302ed..f197f1d41b 100644 --- a/api/app/clients/tools/structured/GeminiImageGen.js +++ b/api/app/clients/tools/structured/GeminiImageGen.js @@ -13,8 +13,7 @@ const { getTransactionsConfig, } = require('@librechat/api'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { spendTokens } = require('~/models/spendTokens'); -const { getFiles } = require('~/models/File'); +const { spendTokens, getFiles } = require('~/models'); /** * Configure proxy support for Google APIs diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index d82a0d6930..4b86101425 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -45,7 +45,7 @@ const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { createMCPTool, createMCPTools } = require('~/server/services/MCP'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { getMCPServerTools } = require('~/server/services/Config'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); /** * Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values. diff --git a/api/models/Action.js b/api/models/Action.js deleted file mode 100644 index f14c415d5b..0000000000 --- a/api/models/Action.js +++ /dev/null @@ -1,73 +0,0 @@ -const { Action } = require('~/db/models'); - -/** - * Update an action with new data without overwriting existing properties, - * or create a new action if it doesn't exist. - * - * @param {{ action_id: string, agent_id?: string, assistant_id?: string, user?: string }} searchParams - * @param {Object} updateData - An object containing the properties to update. - * @returns {Promise} The updated or newly created action document as a plain object. - */ -const updateAction = async (searchParams, updateData) => { - const options = { new: true, upsert: true }; - return await Action.findOneAndUpdate(searchParams, updateData, options).lean(); -}; - -/** - * Retrieves all actions that match the given search parameters. - * - * @param {Object} searchParams - The search parameters to find matching actions. - * @param {boolean} includeSensitive - Flag to include sensitive data in the metadata. - * @returns {Promise>} A promise that resolves to an array of action documents as plain objects. - */ -const getActions = async (searchParams, includeSensitive = false) => { - const actions = await Action.find(searchParams).lean(); - - if (!includeSensitive) { - for (let i = 0; i < actions.length; i++) { - const metadata = actions[i].metadata; - if (!metadata) { - continue; - } - - const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret']; - for (let field of sensitiveFields) { - if (metadata[field]) { - delete metadata[field]; - } - } - } - } - - return actions; -}; - -/** - * Deletes an action by params. - * - * @param {{ action_id: string, agent_id?: string, assistant_id?: string, user?: string }} searchParams - * @returns {Promise} The deleted action document as a plain object, or null if no match. - */ -const deleteAction = async (searchParams) => { - return await Action.findOneAndDelete(searchParams).lean(); -}; - -/** - * Deletes actions by params. - * - * @param {Object} searchParams - The search parameters to find the actions to delete. - * @param {string} searchParams.action_id - The ID of the action(s) to delete. - * @param {string} searchParams.user - The user ID of the action's author. - * @returns {Promise} A promise that resolves to the number of deleted action documents. - */ -const deleteActions = async (searchParams) => { - const result = await Action.deleteMany(searchParams); - return result.deletedCount; -}; - -module.exports = { - getActions, - updateAction, - deleteAction, - deleteActions, -}; diff --git a/api/models/Agent.js b/api/models/Agent.js deleted file mode 100644 index 1ddc535e7b..0000000000 --- a/api/models/Agent.js +++ /dev/null @@ -1,890 +0,0 @@ -const mongoose = require('mongoose'); -const crypto = require('node:crypto'); -const { logger } = require('@librechat/data-schemas'); -const { getCustomEndpointConfig } = require('@librechat/api'); -const { - Tools, - ResourceType, - actionDelimiter, - isAgentsEndpoint, - isEphemeralAgentId, - encodeEphemeralAgentId, -} = require('librechat-data-provider'); -const { mcp_all, mcp_delimiter } = require('librechat-data-provider').Constants; -const { - getSoleOwnedResourceIds, - removeAllPermissions, -} = require('~/server/services/PermissionService'); -const { getMCPServerTools } = require('~/server/services/Config'); -const { Agent, AclEntry, User } = require('~/db/models'); -const { getActions } = require('./Action'); - -/** - * Extracts unique MCP server names from tools array - * Tools format: "toolName_mcp_serverName" or "sys__server__sys_mcp_serverName" - * @param {string[]} tools - Array of tool identifiers - * @returns {string[]} Array of unique MCP server names - */ -const extractMCPServerNames = (tools) => { - if (!tools || !Array.isArray(tools)) { - return []; - } - const serverNames = new Set(); - for (const tool of tools) { - if (!tool || !tool.includes(mcp_delimiter)) { - continue; - } - const parts = tool.split(mcp_delimiter); - if (parts.length >= 2) { - serverNames.add(parts[parts.length - 1]); - } - } - return Array.from(serverNames); -}; - -/** - * Create an agent with the provided data. - * @param {Object} agentData - The agent data to create. - * @returns {Promise} The created agent document as a plain object. - * @throws {Error} If the agent creation fails. - */ -const createAgent = async (agentData) => { - const { author: _author, ...versionData } = agentData; - const timestamp = new Date(); - const initialAgentData = { - ...agentData, - versions: [ - { - ...versionData, - createdAt: timestamp, - updatedAt: timestamp, - }, - ], - category: agentData.category || 'general', - mcpServerNames: extractMCPServerNames(agentData.tools), - }; - - return (await Agent.create(initialAgentData)).toObject(); -}; - -/** - * Get an agent document based on the provided ID. - * - * @param {Object} searchParameter - The search parameters to find the agent to update. - * @param {string} searchParameter.id - The ID of the agent to update. - * @param {string} searchParameter.author - The user ID of the agent's author. - * @returns {Promise} The agent document as a plain object, or null if not found. - */ -const getAgent = async (searchParameter) => await Agent.findOne(searchParameter).lean(); - -/** - * Get multiple agent documents based on the provided search parameters. - * - * @param {Object} searchParameter - The search parameters to find agents. - * @returns {Promise} Array of agent documents as plain objects. - */ -const getAgents = async (searchParameter) => await Agent.find(searchParameter).lean(); - -/** - * Load an agent based on the provided ID - * - * @param {Object} params - * @param {ServerRequest} params.req - * @param {string} params.spec - * @param {string} params.agent_id - * @param {string} params.endpoint - * @param {import('@librechat/agents').ClientOptions} [params.model_parameters] - * @returns {Promise} The agent document as a plain object, or null if not found. - */ -const loadEphemeralAgent = async ({ req, spec, endpoint, model_parameters: _m }) => { - const { model, ...model_parameters } = _m; - const modelSpecs = req.config?.modelSpecs?.list; - /** @type {TModelSpec | null} */ - let modelSpec = null; - if (spec != null && spec !== '') { - modelSpec = modelSpecs?.find((s) => s.name === spec) || null; - } - /** @type {TEphemeralAgent | null} */ - const ephemeralAgent = req.body.ephemeralAgent; - const mcpServers = new Set(ephemeralAgent?.mcp); - const userId = req.user?.id; // note: userId cannot be undefined at runtime - if (modelSpec?.mcpServers) { - for (const mcpServer of modelSpec.mcpServers) { - mcpServers.add(mcpServer); - } - } - /** @type {string[]} */ - const tools = []; - if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) { - tools.push(Tools.execute_code); - } - if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) { - tools.push(Tools.file_search); - } - if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) { - tools.push(Tools.web_search); - } - - const addedServers = new Set(); - if (mcpServers.size > 0) { - for (const mcpServer of mcpServers) { - if (addedServers.has(mcpServer)) { - continue; - } - const serverTools = await getMCPServerTools(userId, mcpServer); - if (!serverTools) { - tools.push(`${mcp_all}${mcp_delimiter}${mcpServer}`); - addedServers.add(mcpServer); - continue; - } - tools.push(...Object.keys(serverTools)); - addedServers.add(mcpServer); - } - } - - const instructions = req.body.promptPrefix; - - // Get endpoint config for modelDisplayLabel fallback - const appConfig = req.config; - let endpointConfig = appConfig?.endpoints?.[endpoint]; - if (!isAgentsEndpoint(endpoint) && !endpointConfig) { - try { - endpointConfig = getCustomEndpointConfig({ endpoint, appConfig }); - } catch (err) { - logger.error('[loadEphemeralAgent] Error getting custom endpoint config', err); - } - } - - // For ephemeral agents, use modelLabel if provided, then model spec's label, - // then modelDisplayLabel from endpoint config, otherwise empty string to show model name - const sender = - model_parameters?.modelLabel ?? modelSpec?.label ?? endpointConfig?.modelDisplayLabel ?? ''; - - // Encode ephemeral agent ID with endpoint, model, and computed sender for display - const ephemeralId = encodeEphemeralAgentId({ endpoint, model, sender }); - - const result = { - id: ephemeralId, - instructions, - provider: endpoint, - model_parameters, - model, - tools, - }; - - if (ephemeralAgent?.artifacts != null && ephemeralAgent.artifacts) { - result.artifacts = ephemeralAgent.artifacts; - } - return result; -}; - -/** - * Load an agent based on the provided ID - * - * @param {Object} params - * @param {ServerRequest} params.req - * @param {string} params.spec - * @param {string} params.agent_id - * @param {string} params.endpoint - * @param {import('@librechat/agents').ClientOptions} [params.model_parameters] - * @returns {Promise} The agent document as a plain object, or null if not found. - */ -const loadAgent = async ({ req, spec, agent_id, endpoint, model_parameters }) => { - if (!agent_id) { - return null; - } - if (isEphemeralAgentId(agent_id)) { - return await loadEphemeralAgent({ req, spec, endpoint, model_parameters }); - } - const agent = await getAgent({ - id: agent_id, - }); - - if (!agent) { - return null; - } - - agent.version = agent.versions ? agent.versions.length : 0; - return agent; -}; - -/** - * Check if a version already exists in the versions array, excluding timestamp and author fields - * @param {Object} updateData - The update data to compare - * @param {Object} currentData - The current agent data - * @param {Array} versions - The existing versions array - * @param {string} [actionsHash] - Hash of current action metadata - * @returns {Object|null} - The matching version if found, null otherwise - */ -const isDuplicateVersion = (updateData, currentData, versions, actionsHash = null) => { - if (!versions || versions.length === 0) { - return null; - } - - const excludeFields = [ - '_id', - 'id', - 'createdAt', - 'updatedAt', - 'author', - 'updatedBy', - 'created_at', - 'updated_at', - '__v', - 'versions', - 'actionsHash', // Exclude actionsHash from direct comparison - ]; - - const { $push: _$push, $pull: _$pull, $addToSet: _$addToSet, ...directUpdates } = updateData; - - if (Object.keys(directUpdates).length === 0 && !actionsHash) { - return null; - } - - const wouldBeVersion = { ...currentData, ...directUpdates }; - const lastVersion = versions[versions.length - 1]; - - if (actionsHash && lastVersion.actionsHash !== actionsHash) { - return null; - } - - const allFields = new Set([...Object.keys(wouldBeVersion), ...Object.keys(lastVersion)]); - - const importantFields = Array.from(allFields).filter((field) => !excludeFields.includes(field)); - - let isMatch = true; - for (const field of importantFields) { - const wouldBeValue = wouldBeVersion[field]; - const lastVersionValue = lastVersion[field]; - - // Skip if both are undefined/null - if (!wouldBeValue && !lastVersionValue) { - continue; - } - - // Handle arrays - if (Array.isArray(wouldBeValue) || Array.isArray(lastVersionValue)) { - // Normalize: treat undefined/null as empty array for comparison - let wouldBeArr; - if (Array.isArray(wouldBeValue)) { - wouldBeArr = wouldBeValue; - } else if (wouldBeValue == null) { - wouldBeArr = []; - } else { - wouldBeArr = [wouldBeValue]; - } - - let lastVersionArr; - if (Array.isArray(lastVersionValue)) { - lastVersionArr = lastVersionValue; - } else if (lastVersionValue == null) { - lastVersionArr = []; - } else { - lastVersionArr = [lastVersionValue]; - } - - if (wouldBeArr.length !== lastVersionArr.length) { - isMatch = false; - break; - } - - // Handle arrays of objects - if (wouldBeArr.length > 0 && typeof wouldBeArr[0] === 'object' && wouldBeArr[0] !== null) { - const sortedWouldBe = [...wouldBeArr].map((item) => JSON.stringify(item)).sort(); - const sortedVersion = [...lastVersionArr].map((item) => JSON.stringify(item)).sort(); - - if (!sortedWouldBe.every((item, i) => item === sortedVersion[i])) { - isMatch = false; - break; - } - } else { - const sortedWouldBe = [...wouldBeArr].sort(); - const sortedVersion = [...lastVersionArr].sort(); - - if (!sortedWouldBe.every((item, i) => item === sortedVersion[i])) { - isMatch = false; - break; - } - } - } - // Handle objects - else if (typeof wouldBeValue === 'object' && wouldBeValue !== null) { - const lastVersionObj = - typeof lastVersionValue === 'object' && lastVersionValue !== null ? lastVersionValue : {}; - - // For empty objects, normalize the comparison - const wouldBeKeys = Object.keys(wouldBeValue); - const lastVersionKeys = Object.keys(lastVersionObj); - - // If both are empty objects, they're equal - if (wouldBeKeys.length === 0 && lastVersionKeys.length === 0) { - continue; - } - - // Otherwise do a deep comparison - if (JSON.stringify(wouldBeValue) !== JSON.stringify(lastVersionObj)) { - isMatch = false; - break; - } - } - // Handle primitive values - else { - // For primitives, handle the case where one is undefined and the other is a default value - if (wouldBeValue !== lastVersionValue) { - // Special handling for boolean false vs undefined - if ( - typeof wouldBeValue === 'boolean' && - wouldBeValue === false && - lastVersionValue === undefined - ) { - continue; - } - // Special handling for empty string vs undefined - if ( - typeof wouldBeValue === 'string' && - wouldBeValue === '' && - lastVersionValue === undefined - ) { - continue; - } - isMatch = false; - break; - } - } - } - - return isMatch ? lastVersion : null; -}; - -/** - * Update an agent with new data without overwriting existing - * properties, or create a new agent if it doesn't exist. - * When an agent is updated, a copy of the current state will be saved to the versions array. - * - * @param {Object} searchParameter - The search parameters to find the agent to update. - * @param {string} searchParameter.id - The ID of the agent to update. - * @param {string} [searchParameter.author] - The user ID of the agent's author. - * @param {Object} updateData - An object containing the properties to update. - * @param {Object} [options] - Optional configuration object. - * @param {string} [options.updatingUserId] - The ID of the user performing the update (used for tracking non-author updates). - * @param {boolean} [options.forceVersion] - Force creation of a new version even if no fields changed. - * @param {boolean} [options.skipVersioning] - Skip version creation entirely (useful for isolated operations like sharing). - * @returns {Promise} The updated or newly created agent document as a plain object. - * @throws {Error} If the update would create a duplicate version - */ -const updateAgent = async (searchParameter, updateData, options = {}) => { - const { updatingUserId = null, forceVersion = false, skipVersioning = false } = options; - const mongoOptions = { new: true, upsert: false }; - - const currentAgent = await Agent.findOne(searchParameter); - if (currentAgent) { - const { - __v, - _id, - id: __id, - versions, - author: _author, - ...versionData - } = currentAgent.toObject(); - const { $push, $pull, $addToSet, ...directUpdates } = updateData; - - // Sync mcpServerNames when tools are updated - if (directUpdates.tools !== undefined) { - const mcpServerNames = extractMCPServerNames(directUpdates.tools); - directUpdates.mcpServerNames = mcpServerNames; - updateData.mcpServerNames = mcpServerNames; // Also update the original updateData - } - - let actionsHash = null; - - // Generate actions hash if agent has actions - if (currentAgent.actions && currentAgent.actions.length > 0) { - // Extract action IDs from the format "domain_action_id" - const actionIds = currentAgent.actions - .map((action) => { - const parts = action.split(actionDelimiter); - return parts[1]; // Get just the action ID part - }) - .filter(Boolean); - - if (actionIds.length > 0) { - try { - const actions = await getActions( - { - action_id: { $in: actionIds }, - }, - true, - ); // Include sensitive data for hash - - actionsHash = await generateActionMetadataHash(currentAgent.actions, actions); - } catch (error) { - logger.error('Error fetching actions for hash generation:', error); - } - } - } - - const shouldCreateVersion = - !skipVersioning && - (forceVersion || Object.keys(directUpdates).length > 0 || $push || $pull || $addToSet); - - if (shouldCreateVersion) { - const duplicateVersion = isDuplicateVersion(updateData, versionData, versions, actionsHash); - if (duplicateVersion && !forceVersion) { - // No changes detected, return the current agent without creating a new version - const agentObj = currentAgent.toObject(); - agentObj.version = versions.length; - return agentObj; - } - } - - const versionEntry = { - ...versionData, - ...directUpdates, - updatedAt: new Date(), - }; - - // Include actions hash in version if available - if (actionsHash) { - versionEntry.actionsHash = actionsHash; - } - - // Always store updatedBy field to track who made the change - if (updatingUserId) { - versionEntry.updatedBy = new mongoose.Types.ObjectId(updatingUserId); - } - - if (shouldCreateVersion) { - updateData.$push = { - ...($push || {}), - versions: versionEntry, - }; - } - } - - return Agent.findOneAndUpdate(searchParameter, updateData, mongoOptions).lean(); -}; - -/** - * Modifies an agent with the resource file id. - * @param {object} params - * @param {ServerRequest} params.req - * @param {string} params.agent_id - * @param {string} params.tool_resource - * @param {string} params.file_id - * @returns {Promise} The updated agent. - */ -const addAgentResourceFile = async ({ req, agent_id, tool_resource, file_id }) => { - const searchParameter = { id: agent_id }; - let agent = await getAgent(searchParameter); - if (!agent) { - throw new Error('Agent not found for adding resource file'); - } - const fileIdsPath = `tool_resources.${tool_resource}.file_ids`; - await Agent.updateOne( - { - id: agent_id, - [`${fileIdsPath}`]: { $exists: false }, - }, - { - $set: { - [`${fileIdsPath}`]: [], - }, - }, - ); - - const updateData = { - $addToSet: { - tools: tool_resource, - [fileIdsPath]: file_id, - }, - }; - - const updatedAgent = await updateAgent(searchParameter, updateData, { - updatingUserId: req?.user?.id, - }); - if (updatedAgent) { - return updatedAgent; - } else { - throw new Error('Agent not found for adding resource file'); - } -}; - -/** - * Removes multiple resource files from an agent using atomic operations. - * @param {object} params - * @param {string} params.agent_id - * @param {Array<{tool_resource: string, file_id: string}>} params.files - * @returns {Promise} The updated agent. - * @throws {Error} If the agent is not found or update fails. - */ -const removeAgentResourceFiles = async ({ agent_id, files }) => { - const searchParameter = { id: agent_id }; - - // Group files to remove by resource - const filesByResource = files.reduce((acc, { tool_resource, file_id }) => { - if (!acc[tool_resource]) { - acc[tool_resource] = []; - } - acc[tool_resource].push(file_id); - return acc; - }, {}); - - const pullAllOps = {}; - const resourcesToCheck = new Set(); - for (const [resource, fileIds] of Object.entries(filesByResource)) { - const fileIdsPath = `tool_resources.${resource}.file_ids`; - pullAllOps[fileIdsPath] = fileIds; - resourcesToCheck.add(resource); - } - - const updatePullData = { $pullAll: pullAllOps }; - const agentAfterPull = await Agent.findOneAndUpdate(searchParameter, updatePullData, { - new: true, - }).lean(); - - if (!agentAfterPull) { - // Agent might have been deleted concurrently, or never existed. - // Check if it existed before trying to throw. - const agentExists = await getAgent(searchParameter); - if (!agentExists) { - throw new Error('Agent not found for removing resource files'); - } - // If it existed but findOneAndUpdate returned null, something else went wrong. - throw new Error('Failed to update agent during file removal (pull step)'); - } - - // Return the agent state directly after the $pull operation. - // Skipping the $unset step for now to simplify and test core $pull atomicity. - // Empty arrays might remain, but the removal itself should be correct. - return agentAfterPull; -}; - -/** - * Deletes an agent based on the provided ID. - * - * @param {Object} searchParameter - The search parameters to find the agent to delete. - * @param {string} searchParameter.id - The ID of the agent to delete. - * @param {string} [searchParameter.author] - The user ID of the agent's author. - * @returns {Promise} Resolves when the agent has been successfully deleted. - */ -const deleteAgent = async (searchParameter) => { - const agent = await Agent.findOneAndDelete(searchParameter); - if (agent) { - await Promise.all([ - removeAllPermissions({ - resourceType: ResourceType.AGENT, - resourceId: agent._id, - }), - removeAllPermissions({ - resourceType: ResourceType.REMOTE_AGENT, - resourceId: agent._id, - }), - ]); - try { - await Agent.updateMany({ 'edges.to': agent.id }, { $pull: { edges: { to: agent.id } } }); - } catch (error) { - logger.error('[deleteAgent] Error removing agent from handoff edges', error); - } - try { - await User.updateMany( - { 'favorites.agentId': agent.id }, - { $pull: { favorites: { agentId: agent.id } } }, - ); - } catch (error) { - logger.error('[deleteAgent] Error removing agent from user favorites', error); - } - } - return agent; -}; - -/** - * Deletes agents solely owned by the user and cleans up their ACLs/project references. - * Agents with other owners are left intact; the caller is responsible for - * removing the user's own ACL principal entries separately. - * - * Also handles legacy (pre-ACL) agents that only have the author field set, - * ensuring they are not orphaned if no permission migration has been run. - * @param {string} userId - The ID of the user whose agents should be deleted. - * @returns {Promise} - */ -const deleteUserAgents = async (userId) => { - try { - const userObjectId = new mongoose.Types.ObjectId(userId); - const soleOwnedObjectIds = await getSoleOwnedResourceIds(userObjectId, [ - ResourceType.AGENT, - ResourceType.REMOTE_AGENT, - ]); - - const authoredAgents = await Agent.find({ author: userObjectId }).select('id _id').lean(); - - const migratedEntries = - authoredAgents.length > 0 - ? await AclEntry.find({ - resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, - resourceId: { $in: authoredAgents.map((a) => a._id) }, - }) - .select('resourceId') - .lean() - : []; - const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString())); - const legacyAgents = authoredAgents.filter((a) => !migratedIds.has(a._id.toString())); - - /** resourceId is the MongoDB _id; agent.id is the string identifier for project/edge queries */ - const soleOwnedAgents = - soleOwnedObjectIds.length > 0 - ? await Agent.find({ _id: { $in: soleOwnedObjectIds } }) - .select('id _id') - .lean() - : []; - - const allAgents = [...soleOwnedAgents, ...legacyAgents]; - - if (allAgents.length === 0) { - return; - } - - const agentIds = allAgents.map((agent) => agent.id); - const agentObjectIds = allAgents.map((agent) => agent._id); - - await AclEntry.deleteMany({ - resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, - resourceId: { $in: agentObjectIds }, - }); - - try { - await Agent.updateMany( - { 'edges.to': { $in: agentIds } }, - { $pull: { edges: { to: { $in: agentIds } } } }, - ); - } catch (error) { - logger.error('[deleteUserAgents] Error removing agents from handoff edges', error); - } - - try { - await User.updateMany( - { 'favorites.agentId': { $in: agentIds } }, - { $pull: { favorites: { agentId: { $in: agentIds } } } }, - ); - } catch (error) { - logger.error('[deleteUserAgents] Error removing agents from user favorites', error); - } - - await Agent.deleteMany({ _id: { $in: agentObjectIds } }); - } catch (error) { - logger.error('[deleteUserAgents] General error:', error); - } -}; - -/** - * Get agents by accessible IDs with optional cursor-based pagination. - * @param {Object} params - The parameters for getting accessible agents. - * @param {Array} [params.accessibleIds] - Array of agent ObjectIds the user has ACL access to. - * @param {Object} [params.otherParams] - Additional query parameters (including author filter). - * @param {number} [params.limit] - Number of agents to return (max 100). If not provided, returns all agents. - * @param {string} [params.after] - Cursor for pagination - get agents after this cursor. // base64 encoded JSON string with updatedAt and _id. - * @returns {Promise} A promise that resolves to an object containing the agents data and pagination info. - */ -const getListAgentsByAccess = async ({ - accessibleIds = [], - otherParams = {}, - limit = null, - after = null, -}) => { - const isPaginated = limit !== null && limit !== undefined; - const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null; - - // Build base query combining ACL accessible agents with other filters - const baseQuery = { ...otherParams, _id: { $in: accessibleIds } }; - - // Add cursor condition - if (after) { - try { - const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8')); - const { updatedAt, _id } = cursor; - - const cursorCondition = { - $or: [ - { updatedAt: { $lt: new Date(updatedAt) } }, - { updatedAt: new Date(updatedAt), _id: { $gt: new mongoose.Types.ObjectId(_id) } }, - ], - }; - - // Merge cursor condition with base query - if (Object.keys(baseQuery).length > 0) { - baseQuery.$and = [{ ...baseQuery }, cursorCondition]; - // Remove the original conditions from baseQuery to avoid duplication - Object.keys(baseQuery).forEach((key) => { - if (key !== '$and') delete baseQuery[key]; - }); - } else { - Object.assign(baseQuery, cursorCondition); - } - } catch (error) { - logger.warn('Invalid cursor:', error.message); - } - } - - let query = Agent.find(baseQuery, { - id: 1, - _id: 1, - name: 1, - avatar: 1, - author: 1, - description: 1, - updatedAt: 1, - category: 1, - support_contact: 1, - is_promoted: 1, - }).sort({ updatedAt: -1, _id: 1 }); - - // Only apply limit if pagination is requested - if (isPaginated) { - query = query.limit(normalizedLimit + 1); - } - - const agents = await query.lean(); - - const hasMore = isPaginated ? agents.length > normalizedLimit : false; - const data = (isPaginated ? agents.slice(0, normalizedLimit) : agents).map((agent) => { - if (agent.author) { - agent.author = agent.author.toString(); - } - return agent; - }); - - // Generate next cursor only if paginated - let nextCursor = null; - if (isPaginated && hasMore && data.length > 0) { - const lastAgent = agents[normalizedLimit - 1]; - nextCursor = Buffer.from( - JSON.stringify({ - updatedAt: lastAgent.updatedAt.toISOString(), - _id: lastAgent._id.toString(), - }), - ).toString('base64'); - } - - return { - object: 'list', - data, - first_id: data.length > 0 ? data[0].id : null, - last_id: data.length > 0 ? data[data.length - 1].id : null, - has_more: hasMore, - after: nextCursor, - }; -}; - -/** - * Reverts an agent to a specific version in its version history. - * @param {Object} searchParameter - The search parameters to find the agent to revert. - * @param {string} searchParameter.id - The ID of the agent to revert. - * @param {string} [searchParameter.author] - The user ID of the agent's author. - * @param {number} versionIndex - The index of the version to revert to in the versions array. - * @returns {Promise} The updated agent document after reverting. - * @throws {Error} If the agent is not found or the specified version does not exist. - */ -const revertAgentVersion = async (searchParameter, versionIndex) => { - const agent = await Agent.findOne(searchParameter); - if (!agent) { - throw new Error('Agent not found'); - } - - if (!agent.versions || !agent.versions[versionIndex]) { - throw new Error(`Version ${versionIndex} not found`); - } - - const revertToVersion = agent.versions[versionIndex]; - - const updateData = { - ...revertToVersion, - }; - - delete updateData._id; - delete updateData.id; - delete updateData.versions; - delete updateData.author; - delete updateData.updatedBy; - - return Agent.findOneAndUpdate(searchParameter, updateData, { new: true }).lean(); -}; - -/** - * Generates a hash of action metadata for version comparison - * @param {string[]} actionIds - Array of action IDs in format "domain_action_id" - * @param {Action[]} actions - Array of action documents - * @returns {Promise} - SHA256 hash of the action metadata - */ -const generateActionMetadataHash = async (actionIds, actions) => { - if (!actionIds || actionIds.length === 0) { - return ''; - } - - // Create a map of action_id to metadata for quick lookup - const actionMap = new Map(); - actions.forEach((action) => { - actionMap.set(action.action_id, action.metadata); - }); - - // Sort action IDs for consistent hashing - const sortedActionIds = [...actionIds].sort(); - - // Build a deterministic string representation of all action metadata - const metadataString = sortedActionIds - .map((actionFullId) => { - // Extract just the action_id part (after the delimiter) - const parts = actionFullId.split(actionDelimiter); - const actionId = parts[1]; - - const metadata = actionMap.get(actionId); - if (!metadata) { - return `${actionId}:null`; - } - - // Sort metadata keys for deterministic output - const sortedKeys = Object.keys(metadata).sort(); - const metadataStr = sortedKeys - .map((key) => `${key}:${JSON.stringify(metadata[key])}`) - .join(','); - return `${actionId}:{${metadataStr}}`; - }) - .join(';'); - - // Use Web Crypto API to generate hash - const encoder = new TextEncoder(); - const data = encoder.encode(metadataString); - const hashBuffer = await crypto.webcrypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); - - return hashHex; -}; -/** - * Counts the number of promoted agents. - * @returns {Promise} - The count of promoted agents - */ -const countPromotedAgents = async () => { - const count = await Agent.countDocuments({ is_promoted: true }); - return count; -}; - -/** - * Load a default agent based on the endpoint - * @param {string} endpoint - * @returns {Agent | null} - */ - -module.exports = { - getAgent, - getAgents, - loadAgent, - createAgent, - updateAgent, - deleteAgent, - deleteUserAgents, - revertAgentVersion, - countPromotedAgents, - addAgentResourceFile, - getListAgentsByAccess, - removeAgentResourceFiles, - generateActionMetadataHash, -}; diff --git a/api/models/Assistant.js b/api/models/Assistant.js deleted file mode 100644 index be94d35d7d..0000000000 --- a/api/models/Assistant.js +++ /dev/null @@ -1,62 +0,0 @@ -const { Assistant } = require('~/db/models'); - -/** - * Update an assistant with new data without overwriting existing properties, - * or create a new assistant if it doesn't exist. - * - * @param {Object} searchParams - The search parameters to find the assistant to update. - * @param {string} searchParams.assistant_id - The ID of the assistant to update. - * @param {string} searchParams.user - The user ID of the assistant's author. - * @param {Object} updateData - An object containing the properties to update. - * @returns {Promise} The updated or newly created assistant document as a plain object. - */ -const updateAssistantDoc = async (searchParams, updateData) => { - const options = { new: true, upsert: true }; - return await Assistant.findOneAndUpdate(searchParams, updateData, options).lean(); -}; - -/** - * Retrieves an assistant document based on the provided ID. - * - * @param {Object} searchParams - The search parameters to find the assistant to update. - * @param {string} searchParams.assistant_id - The ID of the assistant to update. - * @param {string} searchParams.user - The user ID of the assistant's author. - * @returns {Promise} The assistant document as a plain object, or null if not found. - */ -const getAssistant = async (searchParams) => await Assistant.findOne(searchParams).lean(); - -/** - * Retrieves all assistants that match the given search parameters. - * - * @param {Object} searchParams - The search parameters to find matching assistants. - * @param {Object} [select] - Optional. Specifies which document fields to include or exclude. - * @returns {Promise>} A promise that resolves to an array of assistant documents as plain objects. - */ -const getAssistants = async (searchParams, select = null) => { - let query = Assistant.find(searchParams); - - if (select) { - query = query.select(select); - } - - return await query.lean(); -}; - -/** - * Deletes an assistant based on the provided ID. - * - * @param {Object} searchParams - The search parameters to find the assistant to delete. - * @param {string} searchParams.assistant_id - The ID of the assistant to delete. - * @param {string} searchParams.user - The user ID of the assistant's author. - * @returns {Promise} Resolves when the assistant has been successfully deleted. - */ -const deleteAssistant = async (searchParams) => { - return await Assistant.findOneAndDelete(searchParams); -}; - -module.exports = { - updateAssistantDoc, - deleteAssistant, - getAssistants, - getAssistant, -}; diff --git a/api/models/Banner.js b/api/models/Banner.js deleted file mode 100644 index 42ad1599ed..0000000000 --- a/api/models/Banner.js +++ /dev/null @@ -1,28 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { Banner } = require('~/db/models'); - -/** - * Retrieves the current active banner. - * @returns {Promise} The active banner object or null if no active banner is found. - */ -const getBanner = async (user) => { - try { - const now = new Date(); - const banner = await Banner.findOne({ - displayFrom: { $lte: now }, - $or: [{ displayTo: { $gte: now } }, { displayTo: null }], - type: 'banner', - }).lean(); - - if (!banner || banner.isPublic || user) { - return banner; - } - - return null; - } catch (error) { - logger.error('[getBanners] Error getting banners', error); - throw new Error('Error getting banners'); - } -}; - -module.exports = { getBanner }; diff --git a/api/models/Categories.js b/api/models/Categories.js deleted file mode 100644 index 34bd2d8ed2..0000000000 --- a/api/models/Categories.js +++ /dev/null @@ -1,57 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); - -const options = [ - { - label: 'com_ui_idea', - value: 'idea', - }, - { - label: 'com_ui_travel', - value: 'travel', - }, - { - label: 'com_ui_teach_or_explain', - value: 'teach_or_explain', - }, - { - label: 'com_ui_write', - value: 'write', - }, - { - label: 'com_ui_shop', - value: 'shop', - }, - { - label: 'com_ui_code', - value: 'code', - }, - { - label: 'com_ui_misc', - value: 'misc', - }, - { - label: 'com_ui_roleplay', - value: 'roleplay', - }, - { - label: 'com_ui_finance', - value: 'finance', - }, -]; - -module.exports = { - /** - * Retrieves the categories asynchronously. - * @returns {Promise} An array of category objects. - * @throws {Error} If there is an error retrieving the categories. - */ - getCategories: async () => { - try { - // const categories = await Categories.find(); - return options; - } catch (error) { - logger.error('Error getting categories', error); - return []; - } - }, -}; diff --git a/api/models/Conversation.js b/api/models/Conversation.js deleted file mode 100644 index 121eaa9696..0000000000 --- a/api/models/Conversation.js +++ /dev/null @@ -1,373 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { createTempChatExpirationDate } = require('@librechat/api'); -const { getMessages, deleteMessages } = require('./Message'); -const { Conversation } = require('~/db/models'); - -/** - * Searches for a conversation by conversationId and returns a lean document with only conversationId and user. - * @param {string} conversationId - The conversation's ID. - * @returns {Promise<{conversationId: string, user: string} | null>} The conversation object with selected fields or null if not found. - */ -const searchConversation = async (conversationId) => { - try { - return await Conversation.findOne({ conversationId }, 'conversationId user').lean(); - } catch (error) { - logger.error('[searchConversation] Error searching conversation', error); - throw new Error('Error searching conversation'); - } -}; - -/** - * Retrieves a single conversation for a given user and conversation ID. - * @param {string} user - The user's ID. - * @param {string} conversationId - The conversation's ID. - * @returns {Promise} The conversation object. - */ -const getConvo = async (user, conversationId) => { - try { - return await Conversation.findOne({ user, conversationId }).lean(); - } catch (error) { - logger.error('[getConvo] Error getting single conversation', error); - throw new Error('Error getting single conversation'); - } -}; - -const deleteNullOrEmptyConversations = async () => { - try { - const filter = { - $or: [ - { conversationId: null }, - { conversationId: '' }, - { conversationId: { $exists: false } }, - ], - }; - - const result = await Conversation.deleteMany(filter); - - // Delete associated messages - const messageDeleteResult = await deleteMessages(filter); - - logger.info( - `[deleteNullOrEmptyConversations] Deleted ${result.deletedCount} conversations and ${messageDeleteResult.deletedCount} messages`, - ); - - return { - conversations: result, - messages: messageDeleteResult, - }; - } catch (error) { - logger.error('[deleteNullOrEmptyConversations] Error deleting conversations', error); - throw new Error('Error deleting conversations with null or empty conversationId'); - } -}; - -/** - * Searches for a conversation by conversationId and returns associated file ids. - * @param {string} conversationId - The conversation's ID. - * @returns {Promise} - */ -const getConvoFiles = async (conversationId) => { - try { - return (await Conversation.findOne({ conversationId }, 'files').lean())?.files ?? []; - } catch (error) { - logger.error('[getConvoFiles] Error getting conversation files', error); - throw new Error('Error getting conversation files'); - } -}; - -module.exports = { - getConvoFiles, - searchConversation, - deleteNullOrEmptyConversations, - /** - * Saves a conversation to the database. - * @param {Object} req - The request object. - * @param {string} conversationId - The conversation's ID. - * @param {Object} metadata - Additional metadata to log for operation. - * @returns {Promise} The conversation object. - */ - saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => { - try { - if (metadata?.context) { - logger.debug(`[saveConvo] ${metadata.context}`); - } - - const messages = await getMessages({ conversationId }, '_id'); - const update = { ...convo, messages, user: req.user.id }; - - if (newConversationId) { - update.conversationId = newConversationId; - } - - if (req?.body?.isTemporary) { - try { - const appConfig = req.config; - update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig); - } catch (err) { - logger.error('Error creating temporary chat expiration date:', err); - logger.info(`---\`saveConvo\` context: ${metadata?.context}`); - update.expiredAt = null; - } - } else { - update.expiredAt = null; - } - - /** @type {{ $set: Partial; $unset?: Record }} */ - const updateOperation = { $set: update }; - if (metadata && metadata.unsetFields && Object.keys(metadata.unsetFields).length > 0) { - updateOperation.$unset = metadata.unsetFields; - } - - /** Note: the resulting Model object is necessary for Meilisearch operations */ - const conversation = await Conversation.findOneAndUpdate( - { conversationId, user: req.user.id }, - updateOperation, - { - new: true, - upsert: metadata?.noUpsert !== true, - }, - ); - - if (!conversation) { - logger.debug('[saveConvo] Conversation not found, skipping update'); - return null; - } - - return conversation.toObject(); - } catch (error) { - logger.error('[saveConvo] Error saving conversation', error); - if (metadata && metadata?.context) { - logger.info(`[saveConvo] ${metadata.context}`); - } - return { message: 'Error saving conversation' }; - } - }, - bulkSaveConvos: async (conversations) => { - try { - const bulkOps = conversations.map((convo) => ({ - updateOne: { - filter: { conversationId: convo.conversationId, user: convo.user }, - update: convo, - upsert: true, - timestamps: false, - }, - })); - - const result = await Conversation.bulkWrite(bulkOps); - return result; - } catch (error) { - logger.error('[bulkSaveConvos] Error saving conversations in bulk', error); - throw new Error('Failed to save conversations in bulk.'); - } - }, - getConvosByCursor: async ( - user, - { - cursor, - limit = 25, - isArchived = false, - tags, - search, - sortBy = 'updatedAt', - sortDirection = 'desc', - } = {}, - ) => { - const filters = [{ user }]; - if (isArchived) { - filters.push({ isArchived: true }); - } else { - filters.push({ $or: [{ isArchived: false }, { isArchived: { $exists: false } }] }); - } - - if (Array.isArray(tags) && tags.length > 0) { - filters.push({ tags: { $in: tags } }); - } - - filters.push({ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] }); - - if (search) { - try { - const meiliResults = await Conversation.meiliSearch(search, { filter: `user = "${user}"` }); - const matchingIds = Array.isArray(meiliResults.hits) - ? meiliResults.hits.map((result) => result.conversationId) - : []; - if (!matchingIds.length) { - return { conversations: [], nextCursor: null }; - } - filters.push({ conversationId: { $in: matchingIds } }); - } catch (error) { - logger.error('[getConvosByCursor] Error during meiliSearch', error); - throw new Error('Error during meiliSearch'); - } - } - - const validSortFields = ['title', 'createdAt', 'updatedAt']; - if (!validSortFields.includes(sortBy)) { - throw new Error( - `Invalid sortBy field: ${sortBy}. Must be one of ${validSortFields.join(', ')}`, - ); - } - const finalSortBy = sortBy; - const finalSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; - - let cursorFilter = null; - if (cursor) { - try { - const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString()); - const { primary, secondary } = decoded; - const primaryValue = finalSortBy === 'title' ? primary : new Date(primary); - const secondaryValue = new Date(secondary); - const op = finalSortDirection === 'asc' ? '$gt' : '$lt'; - - cursorFilter = { - $or: [ - { [finalSortBy]: { [op]: primaryValue } }, - { - [finalSortBy]: primaryValue, - updatedAt: { [op]: secondaryValue }, - }, - ], - }; - } catch (_err) { - logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning'); - } - if (cursorFilter) { - filters.push(cursorFilter); - } - } - - const query = filters.length === 1 ? filters[0] : { $and: filters }; - - try { - const sortOrder = finalSortDirection === 'asc' ? 1 : -1; - const sortObj = { [finalSortBy]: sortOrder }; - - if (finalSortBy !== 'updatedAt') { - sortObj.updatedAt = sortOrder; - } - - const convos = await Conversation.find(query) - .select( - 'conversationId endpoint title createdAt updatedAt user model agent_id assistant_id spec iconURL', - ) - .sort(sortObj) - .limit(limit + 1) - .lean(); - - let nextCursor = null; - if (convos.length > limit) { - convos.pop(); // Remove extra item used to detect next page - // Create cursor from the last RETURNED item (not the popped one) - const lastReturned = convos[convos.length - 1]; - const primaryValue = lastReturned[finalSortBy]; - const primaryStr = finalSortBy === 'title' ? primaryValue : primaryValue.toISOString(); - const secondaryStr = lastReturned.updatedAt.toISOString(); - const composite = { primary: primaryStr, secondary: secondaryStr }; - nextCursor = Buffer.from(JSON.stringify(composite)).toString('base64'); - } - - return { conversations: convos, nextCursor }; - } catch (error) { - logger.error('[getConvosByCursor] Error getting conversations', error); - throw new Error('Error getting conversations'); - } - }, - getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => { - try { - if (!convoIds?.length) { - return { conversations: [], nextCursor: null, convoMap: {} }; - } - - const conversationIds = convoIds.map((convo) => convo.conversationId); - - const results = await Conversation.find({ - user, - conversationId: { $in: conversationIds }, - $or: [{ expiredAt: { $exists: false } }, { expiredAt: null }], - }).lean(); - - results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); - - let filtered = results; - if (cursor && cursor !== 'start') { - const cursorDate = new Date(cursor); - filtered = results.filter((convo) => new Date(convo.updatedAt) < cursorDate); - } - - const limited = filtered.slice(0, limit + 1); - let nextCursor = null; - if (limited.length > limit) { - limited.pop(); // Remove extra item used to detect next page - // Create cursor from the last RETURNED item (not the popped one) - nextCursor = limited[limited.length - 1].updatedAt.toISOString(); - } - - const convoMap = {}; - limited.forEach((convo) => { - convoMap[convo.conversationId] = convo; - }); - - return { conversations: limited, nextCursor, convoMap }; - } catch (error) { - logger.error('[getConvosQueried] Error getting conversations', error); - throw new Error('Error fetching conversations'); - } - }, - getConvo, - /* chore: this method is not properly error handled */ - getConvoTitle: async (user, conversationId) => { - try { - const convo = await getConvo(user, conversationId); - /* ChatGPT Browser was triggering error here due to convo being saved later */ - if (convo && !convo.title) { - return null; - } else { - // TypeError: Cannot read properties of null (reading 'title') - return convo?.title || 'New Chat'; - } - } catch (error) { - logger.error('[getConvoTitle] Error getting conversation title', error); - throw new Error('Error getting conversation title'); - } - }, - /** - * Asynchronously deletes conversations and associated messages for a given user and filter. - * - * @async - * @function - * @param {string|ObjectId} user - The user's ID. - * @param {Object} filter - Additional filter criteria for the conversations to be deleted. - * @returns {Promise<{ n: number, ok: number, deletedCount: number, messages: { n: number, ok: number, deletedCount: number } }>} - * An object containing the count of deleted conversations and associated messages. - * @throws {Error} Throws an error if there's an issue with the database operations. - * - * @example - * const user = 'someUserId'; - * const filter = { someField: 'someValue' }; - * const result = await deleteConvos(user, filter); - * logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } } - */ - deleteConvos: async (user, filter) => { - try { - const userFilter = { ...filter, user }; - const conversations = await Conversation.find(userFilter).select('conversationId'); - const conversationIds = conversations.map((c) => c.conversationId); - - if (!conversationIds.length) { - throw new Error('Conversation not found or already deleted.'); - } - - const deleteConvoResult = await Conversation.deleteMany(userFilter); - - const deleteMessagesResult = await deleteMessages({ - conversationId: { $in: conversationIds }, - user, - }); - - return { ...deleteConvoResult, messages: deleteMessagesResult }; - } catch (error) { - logger.error('[deleteConvos] Error deleting conversations and messages', error); - throw error; - } - }, -}; diff --git a/api/models/ConversationTag.js b/api/models/ConversationTag.js deleted file mode 100644 index 99d0608a66..0000000000 --- a/api/models/ConversationTag.js +++ /dev/null @@ -1,284 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { ConversationTag, Conversation } = require('~/db/models'); - -/** - * Retrieves all conversation tags for a user. - * @param {string} user - The user ID. - * @returns {Promise} An array of conversation tags. - */ -const getConversationTags = async (user) => { - try { - return await ConversationTag.find({ user }).sort({ position: 1 }).lean(); - } catch (error) { - logger.error('[getConversationTags] Error getting conversation tags', error); - throw new Error('Error getting conversation tags'); - } -}; - -/** - * Creates a new conversation tag. - * @param {string} user - The user ID. - * @param {Object} data - The tag data. - * @param {string} data.tag - The tag name. - * @param {string} [data.description] - The tag description. - * @param {boolean} [data.addToConversation] - Whether to add the tag to a conversation. - * @param {string} [data.conversationId] - The conversation ID to add the tag to. - * @returns {Promise} The created tag. - */ -const createConversationTag = async (user, data) => { - try { - const { tag, description, addToConversation, conversationId } = data; - - const existingTag = await ConversationTag.findOne({ user, tag }).lean(); - if (existingTag) { - return existingTag; - } - - const maxPosition = await ConversationTag.findOne({ user }).sort('-position').lean(); - const position = (maxPosition?.position || 0) + 1; - - const newTag = await ConversationTag.findOneAndUpdate( - { tag, user }, - { - tag, - user, - count: addToConversation ? 1 : 0, - position, - description, - $setOnInsert: { createdAt: new Date() }, - }, - { - new: true, - upsert: true, - lean: true, - }, - ); - - if (addToConversation && conversationId) { - await Conversation.findOneAndUpdate( - { user, conversationId }, - { $addToSet: { tags: tag } }, - { new: true }, - ); - } - - return newTag; - } catch (error) { - logger.error('[createConversationTag] Error creating conversation tag', error); - throw new Error('Error creating conversation tag'); - } -}; - -/** - * Updates an existing conversation tag. - * @param {string} user - The user ID. - * @param {string} oldTag - The current tag name. - * @param {Object} data - The updated tag data. - * @param {string} [data.tag] - The new tag name. - * @param {string} [data.description] - The updated description. - * @param {number} [data.position] - The new position. - * @returns {Promise} The updated tag. - */ -const updateConversationTag = async (user, oldTag, data) => { - try { - const { tag: newTag, description, position } = data; - - const existingTag = await ConversationTag.findOne({ user, tag: oldTag }).lean(); - if (!existingTag) { - return null; - } - - if (newTag && newTag !== oldTag) { - const tagAlreadyExists = await ConversationTag.findOne({ user, tag: newTag }).lean(); - if (tagAlreadyExists) { - throw new Error('Tag already exists'); - } - - await Conversation.updateMany({ user, tags: oldTag }, { $set: { 'tags.$': newTag } }); - } - - const updateData = {}; - if (newTag) { - updateData.tag = newTag; - } - if (description !== undefined) { - updateData.description = description; - } - if (position !== undefined) { - await adjustPositions(user, existingTag.position, position); - updateData.position = position; - } - - return await ConversationTag.findOneAndUpdate({ user, tag: oldTag }, updateData, { - new: true, - lean: true, - }); - } catch (error) { - logger.error('[updateConversationTag] Error updating conversation tag', error); - throw new Error('Error updating conversation tag'); - } -}; - -/** - * Adjusts positions of tags when a tag's position is changed. - * @param {string} user - The user ID. - * @param {number} oldPosition - The old position of the tag. - * @param {number} newPosition - The new position of the tag. - * @returns {Promise} - */ -const adjustPositions = async (user, oldPosition, newPosition) => { - if (oldPosition === newPosition) { - return; - } - - const update = oldPosition < newPosition ? { $inc: { position: -1 } } : { $inc: { position: 1 } }; - const position = - oldPosition < newPosition - ? { - $gt: Math.min(oldPosition, newPosition), - $lte: Math.max(oldPosition, newPosition), - } - : { - $gte: Math.min(oldPosition, newPosition), - $lt: Math.max(oldPosition, newPosition), - }; - - await ConversationTag.updateMany( - { - user, - position, - }, - update, - ); -}; - -/** - * Deletes a conversation tag. - * @param {string} user - The user ID. - * @param {string} tag - The tag to delete. - * @returns {Promise} The deleted tag. - */ -const deleteConversationTag = async (user, tag) => { - try { - const deletedTag = await ConversationTag.findOneAndDelete({ user, tag }).lean(); - if (!deletedTag) { - return null; - } - - await Conversation.updateMany({ user, tags: tag }, { $pullAll: { tags: [tag] } }); - - await ConversationTag.updateMany( - { user, position: { $gt: deletedTag.position } }, - { $inc: { position: -1 } }, - ); - - return deletedTag; - } catch (error) { - logger.error('[deleteConversationTag] Error deleting conversation tag', error); - throw new Error('Error deleting conversation tag'); - } -}; - -/** - * Updates tags for a specific conversation. - * @param {string} user - The user ID. - * @param {string} conversationId - The conversation ID. - * @param {string[]} tags - The new set of tags for the conversation. - * @returns {Promise} The updated list of tags for the conversation. - */ -const updateTagsForConversation = async (user, conversationId, tags) => { - try { - const conversation = await Conversation.findOne({ user, conversationId }).lean(); - if (!conversation) { - throw new Error('Conversation not found'); - } - - const oldTags = new Set(conversation.tags); - const newTags = new Set(tags); - - const addedTags = [...newTags].filter((tag) => !oldTags.has(tag)); - const removedTags = [...oldTags].filter((tag) => !newTags.has(tag)); - - const bulkOps = []; - - for (const tag of addedTags) { - bulkOps.push({ - updateOne: { - filter: { user, tag }, - update: { $inc: { count: 1 } }, - upsert: true, - }, - }); - } - - for (const tag of removedTags) { - bulkOps.push({ - updateOne: { - filter: { user, tag }, - update: { $inc: { count: -1 } }, - }, - }); - } - - if (bulkOps.length > 0) { - await ConversationTag.bulkWrite(bulkOps); - } - - const updatedConversation = ( - await Conversation.findOneAndUpdate( - { user, conversationId }, - { $set: { tags: [...newTags] } }, - { new: true }, - ) - ).toObject(); - - return updatedConversation.tags; - } catch (error) { - logger.error('[updateTagsForConversation] Error updating tags', error); - throw new Error('Error updating tags for conversation'); - } -}; - -/** - * Increments tag counts for existing tags only. - * @param {string} user - The user ID. - * @param {string[]} tags - Array of tag names to increment - * @returns {Promise} - */ -const bulkIncrementTagCounts = async (user, tags) => { - if (!tags || tags.length === 0) { - return; - } - - try { - const uniqueTags = [...new Set(tags.filter(Boolean))]; - if (uniqueTags.length === 0) { - return; - } - - const bulkOps = uniqueTags.map((tag) => ({ - updateOne: { - filter: { user, tag }, - update: { $inc: { count: 1 } }, - }, - })); - - const result = await ConversationTag.bulkWrite(bulkOps); - if (result && result.modifiedCount > 0) { - logger.debug( - `user: ${user} | Incremented tag counts - modified ${result.modifiedCount} tags`, - ); - } - } catch (error) { - logger.error('[bulkIncrementTagCounts] Error incrementing tag counts', error); - } -}; - -module.exports = { - getConversationTags, - createConversationTag, - updateConversationTag, - deleteConversationTag, - bulkIncrementTagCounts, - updateTagsForConversation, -}; diff --git a/api/models/File.js b/api/models/File.js deleted file mode 100644 index 1a01ef12f9..0000000000 --- a/api/models/File.js +++ /dev/null @@ -1,250 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { EToolResources, FileContext } = require('librechat-data-provider'); -const { File } = require('~/db/models'); - -/** - * Finds a file by its file_id with additional query options. - * @param {string} file_id - The unique identifier of the file. - * @param {object} options - Query options for filtering, projection, etc. - * @returns {Promise} A promise that resolves to the file document or null. - */ -const findFileById = async (file_id, options = {}) => { - return await File.findOne({ file_id, ...options }).lean(); -}; - -/** - * Retrieves files matching a given filter, sorted by the most recently updated. - * @param {Object} filter - The filter criteria to apply. - * @param {Object} [_sortOptions] - Optional sort parameters. - * @param {Object|String} [selectFields={ text: 0 }] - Fields to include/exclude in the query results. - * Default excludes the 'text' field. - * @returns {Promise>} A promise that resolves to an array of file documents. - */ -const getFiles = async (filter, _sortOptions, selectFields = { text: 0 }) => { - const sortOptions = { updatedAt: -1, ..._sortOptions }; - return await File.find(filter).select(selectFields).sort(sortOptions).lean(); -}; - -/** - * Retrieves tool files (files that are embedded or have a fileIdentifier) from an array of file IDs. - * Note: execute_code files are handled separately by getCodeGeneratedFiles. - * @param {string[]} fileIds - Array of file_id strings to search for - * @param {Set} toolResourceSet - Optional filter for tool resources - * @returns {Promise>} Files that match the criteria - */ -const getToolFilesByIds = async (fileIds, toolResourceSet) => { - if (!fileIds || !fileIds.length || !toolResourceSet?.size) { - return []; - } - - try { - const orConditions = []; - - if (toolResourceSet.has(EToolResources.context)) { - orConditions.push({ text: { $exists: true, $ne: null }, context: FileContext.agents }); - } - if (toolResourceSet.has(EToolResources.file_search)) { - orConditions.push({ embedded: true }); - } - - if (orConditions.length === 0) { - return []; - } - - const filter = { - file_id: { $in: fileIds }, - context: { $ne: FileContext.execute_code }, // Exclude code-generated files - $or: orConditions, - }; - - const selectFields = { text: 0 }; - const sortOptions = { updatedAt: -1 }; - - return await getFiles(filter, sortOptions, selectFields); - } catch (error) { - logger.error('[getToolFilesByIds] Error retrieving tool files:', error); - throw new Error('Error retrieving tool files'); - } -}; - -/** - * Retrieves files generated by code execution for a given conversation. - * These files are stored locally with fileIdentifier metadata for code env re-upload. - * @param {string} conversationId - The conversation ID to search for - * @param {string[]} [messageIds] - Optional array of messageIds to filter by (for linear thread filtering) - * @returns {Promise>} Files generated by code execution in the conversation - */ -const getCodeGeneratedFiles = async (conversationId, messageIds) => { - if (!conversationId) { - return []; - } - - /** messageIds are required for proper thread filtering of code-generated files */ - if (!messageIds || messageIds.length === 0) { - return []; - } - - try { - const filter = { - conversationId, - context: FileContext.execute_code, - messageId: { $exists: true, $in: messageIds }, - 'metadata.fileIdentifier': { $exists: true }, - }; - - const selectFields = { text: 0 }; - const sortOptions = { createdAt: 1 }; - - return await getFiles(filter, sortOptions, selectFields); - } catch (error) { - logger.error('[getCodeGeneratedFiles] Error retrieving code generated files:', error); - return []; - } -}; - -/** - * Retrieves user-uploaded execute_code files (not code-generated) by their file IDs. - * These are files with fileIdentifier metadata but context is NOT execute_code (e.g., agents or message_attachment). - * File IDs should be collected from message.files arrays in the current thread. - * @param {string[]} fileIds - Array of file IDs to fetch (from message.files in the thread) - * @returns {Promise>} User-uploaded execute_code files - */ -const getUserCodeFiles = async (fileIds) => { - if (!fileIds || fileIds.length === 0) { - return []; - } - - try { - const filter = { - file_id: { $in: fileIds }, - context: { $ne: FileContext.execute_code }, - 'metadata.fileIdentifier': { $exists: true }, - }; - - const selectFields = { text: 0 }; - const sortOptions = { createdAt: 1 }; - - return await getFiles(filter, sortOptions, selectFields); - } catch (error) { - logger.error('[getUserCodeFiles] Error retrieving user code files:', error); - return []; - } -}; - -/** - * Creates a new file with a TTL of 1 hour. - * @param {MongoFile} data - The file data to be created, must contain file_id. - * @param {boolean} disableTTL - Whether to disable the TTL. - * @returns {Promise} A promise that resolves to the created file document. - */ -const createFile = async (data, disableTTL) => { - const fileData = { - ...data, - expiresAt: new Date(Date.now() + 3600 * 1000), - }; - - if (disableTTL) { - delete fileData.expiresAt; - } - - return await File.findOneAndUpdate({ file_id: data.file_id }, fileData, { - new: true, - upsert: true, - }).lean(); -}; - -/** - * Updates a file identified by file_id with new data and removes the TTL. - * @param {MongoFile} data - The data to update, must contain file_id. - * @returns {Promise} A promise that resolves to the updated file document. - */ -const updateFile = async (data) => { - const { file_id, ...update } = data; - const updateOperation = { - $set: update, - $unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL - }; - return await File.findOneAndUpdate({ file_id }, updateOperation, { new: true }).lean(); -}; - -/** - * Increments the usage of a file identified by file_id. - * @param {MongoFile} data - The data to update, must contain file_id and the increment value for usage. - * @returns {Promise} A promise that resolves to the updated file document. - */ -const updateFileUsage = async (data) => { - const { file_id, inc = 1 } = data; - const updateOperation = { - $inc: { usage: inc }, - $unset: { expiresAt: '', temp_file_id: '' }, - }; - return await File.findOneAndUpdate({ file_id }, updateOperation, { new: true }).lean(); -}; - -/** - * Deletes a file identified by file_id. - * @param {string} file_id - The unique identifier of the file to delete. - * @returns {Promise} A promise that resolves to the deleted file document or null. - */ -const deleteFile = async (file_id) => { - return await File.findOneAndDelete({ file_id }).lean(); -}; - -/** - * Deletes a file identified by a filter. - * @param {object} filter - The filter criteria to apply. - * @returns {Promise} A promise that resolves to the deleted file document or null. - */ -const deleteFileByFilter = async (filter) => { - return await File.findOneAndDelete(filter).lean(); -}; - -/** - * Deletes multiple files identified by an array of file_ids. - * @param {Array} file_ids - The unique identifiers of the files to delete. - * @returns {Promise} A promise that resolves to the result of the deletion operation. - */ -const deleteFiles = async (file_ids, user) => { - let deleteQuery = { file_id: { $in: file_ids } }; - if (user) { - deleteQuery = { user: user }; - } - return await File.deleteMany(deleteQuery); -}; - -/** - * Batch updates files with new signed URLs in MongoDB - * - * @param {MongoFile[]} updates - Array of updates in the format { file_id, filepath } - * @returns {Promise} - */ -async function batchUpdateFiles(updates) { - if (!updates || updates.length === 0) { - return; - } - - const bulkOperations = updates.map((update) => ({ - updateOne: { - filter: { file_id: update.file_id }, - update: { $set: { filepath: update.filepath } }, - }, - })); - - const result = await File.bulkWrite(bulkOperations); - logger.info(`Updated ${result.modifiedCount} files with new S3 URLs`); -} - -module.exports = { - findFileById, - getFiles, - getToolFilesByIds, - getCodeGeneratedFiles, - getUserCodeFiles, - createFile, - updateFile, - updateFileUsage, - deleteFile, - deleteFiles, - deleteFileByFilter, - batchUpdateFiles, -}; diff --git a/api/models/File.spec.js b/api/models/File.spec.js deleted file mode 100644 index ecb2e21b08..0000000000 --- a/api/models/File.spec.js +++ /dev/null @@ -1,736 +0,0 @@ -const mongoose = require('mongoose'); -const { v4: uuidv4 } = require('uuid'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { createModels, createMethods } = require('@librechat/data-schemas'); -const { - SystemRoles, - ResourceType, - AccessRoleIds, - PrincipalType, -} = require('librechat-data-provider'); -const { grantPermission } = require('~/server/services/PermissionService'); -const { createAgent } = require('./Agent'); - -let File; -let Agent; -let AclEntry; -let User; -let modelsToCleanup = []; -let methods; -let getFiles; -let createFile; -let seedDefaultRoles; - -describe('File Access Control', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - await mongoose.connect(mongoUri); - - // Initialize all models - const models = createModels(mongoose); - - // Track which models we're adding - modelsToCleanup = Object.keys(models); - - // Register models on mongoose.models so methods can access them - const dbModels = require('~/db/models'); - Object.assign(mongoose.models, dbModels); - - File = dbModels.File; - Agent = dbModels.Agent; - AclEntry = dbModels.AclEntry; - User = dbModels.User; - - // Create methods from data-schemas (includes file methods) - methods = createMethods(mongoose); - getFiles = methods.getFiles; - createFile = methods.createFile; - seedDefaultRoles = methods.seedDefaultRoles; - - // Seed default roles - await seedDefaultRoles(); - }); - - afterAll(async () => { - // Clean up all collections before disconnecting - const collections = mongoose.connection.collections; - for (const key in collections) { - await collections[key].deleteMany({}); - } - - // Clear only the models we added - for (const modelName of modelsToCleanup) { - if (mongoose.models[modelName]) { - delete mongoose.models[modelName]; - } - } - - await mongoose.disconnect(); - await mongoServer.stop(); - }); - - beforeEach(async () => { - await File.deleteMany({}); - await Agent.deleteMany({}); - await AclEntry.deleteMany({}); - await User.deleteMany({}); - // Don't delete AccessRole as they are seeded defaults needed for tests - }); - - describe('hasAccessToFilesViaAgent', () => { - it('should efficiently check access for multiple files at once', async () => { - const userId = new mongoose.Types.ObjectId(); - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileIds = [uuidv4(), uuidv4(), uuidv4(), uuidv4()]; - - // Create users - await User.create({ - _id: userId, - email: 'user@example.com', - emailVerified: true, - provider: 'local', - }); - - await User.create({ - _id: authorId, - email: 'author@example.com', - emailVerified: true, - provider: 'local', - }); - - // Create files - for (const fileId of fileIds) { - await createFile({ - user: authorId, - file_id: fileId, - filename: `file-${fileId}.txt`, - filepath: `/uploads/${fileId}`, - }); - } - - // Create agent with only first two files attached - const agent = await createAgent({ - id: agentId, - name: 'Test Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: [fileIds[0], fileIds[1]], - }, - }, - }); - - // Grant EDIT permission to user on the agent - await grantPermission({ - principalType: PrincipalType.USER, - principalId: userId, - resourceType: ResourceType.AGENT, - resourceId: agent._id, - accessRoleId: AccessRoleIds.AGENT_EDITOR, - grantedBy: authorId, - }); - - // Check access for all files - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: userId, - role: SystemRoles.USER, - fileIds, - agentId: agent.id, // Use agent.id which is the custom UUID - }); - - // Should have access only to the first two files - expect(accessMap.get(fileIds[0])).toBe(true); - expect(accessMap.get(fileIds[1])).toBe(true); - expect(accessMap.get(fileIds[2])).toBe(false); - expect(accessMap.get(fileIds[3])).toBe(false); - }); - - it('should only grant author access to files attached to the agent', async () => { - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileIds = [uuidv4(), uuidv4(), uuidv4()]; - - await User.create({ - _id: authorId, - email: 'author@example.com', - emailVerified: true, - provider: 'local', - }); - - await createAgent({ - id: agentId, - name: 'Test Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: [fileIds[0]], - }, - }, - }); - - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: authorId, - role: SystemRoles.USER, - fileIds, - agentId, - }); - - expect(accessMap.get(fileIds[0])).toBe(true); - expect(accessMap.get(fileIds[1])).toBe(false); - expect(accessMap.get(fileIds[2])).toBe(false); - }); - - it('should deny all access when agent has no tool_resources', async () => { - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileId = uuidv4(); - - await User.create({ - _id: authorId, - email: 'author-no-resources@example.com', - emailVerified: true, - provider: 'local', - }); - - await createAgent({ - id: agentId, - name: 'Bare Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - }); - - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: authorId, - role: SystemRoles.USER, - fileIds: [fileId], - agentId, - }); - - expect(accessMap.get(fileId)).toBe(false); - }); - - it('should grant access to files across multiple resource types', async () => { - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileIds = [uuidv4(), uuidv4(), uuidv4()]; - - await User.create({ - _id: authorId, - email: 'author-multi@example.com', - emailVerified: true, - provider: 'local', - }); - - await createAgent({ - id: agentId, - name: 'Multi Resource Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: [fileIds[0]], - }, - execute_code: { - file_ids: [fileIds[1]], - }, - }, - }); - - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: authorId, - role: SystemRoles.USER, - fileIds, - agentId, - }); - - expect(accessMap.get(fileIds[0])).toBe(true); - expect(accessMap.get(fileIds[1])).toBe(true); - expect(accessMap.get(fileIds[2])).toBe(false); - }); - - it('should grant author access to attached files when isDelete is true', async () => { - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const attachedFileId = uuidv4(); - const unattachedFileId = uuidv4(); - - await User.create({ - _id: authorId, - email: 'author-delete@example.com', - emailVerified: true, - provider: 'local', - }); - - await createAgent({ - id: agentId, - name: 'Delete Test Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: [attachedFileId], - }, - }, - }); - - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: authorId, - role: SystemRoles.USER, - fileIds: [attachedFileId, unattachedFileId], - agentId, - isDelete: true, - }); - - expect(accessMap.get(attachedFileId)).toBe(true); - expect(accessMap.get(unattachedFileId)).toBe(false); - }); - - it('should handle non-existent agent gracefully', async () => { - const userId = new mongoose.Types.ObjectId(); - const fileIds = [uuidv4(), uuidv4()]; - - // Create user - await User.create({ - _id: userId, - email: 'user@example.com', - emailVerified: true, - provider: 'local', - }); - - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: userId, - role: SystemRoles.USER, - fileIds, - agentId: 'non-existent-agent', - }); - - // Should have no access to any files - expect(accessMap.get(fileIds[0])).toBe(false); - expect(accessMap.get(fileIds[1])).toBe(false); - }); - - it('should deny access when user only has VIEW permission and needs access for deletion', async () => { - const userId = new mongoose.Types.ObjectId(); - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileIds = [uuidv4(), uuidv4()]; - - // Create users - await User.create({ - _id: userId, - email: 'user@example.com', - emailVerified: true, - provider: 'local', - }); - - await User.create({ - _id: authorId, - email: 'author@example.com', - emailVerified: true, - provider: 'local', - }); - - // Create agent with files - const agent = await createAgent({ - id: agentId, - name: 'View-Only Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: fileIds, - }, - }, - }); - - // Grant only VIEW permission to user on the agent - await grantPermission({ - principalType: PrincipalType.USER, - principalId: userId, - resourceType: ResourceType.AGENT, - resourceId: agent._id, - accessRoleId: AccessRoleIds.AGENT_VIEWER, - grantedBy: authorId, - }); - - // Check access for files - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: userId, - role: SystemRoles.USER, - fileIds, - agentId, - isDelete: true, - }); - - // Should have no access to any files when only VIEW permission - expect(accessMap.get(fileIds[0])).toBe(false); - expect(accessMap.get(fileIds[1])).toBe(false); - }); - - it('should grant access when user has VIEW permission', async () => { - const userId = new mongoose.Types.ObjectId(); - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileIds = [uuidv4(), uuidv4()]; - - // Create users - await User.create({ - _id: userId, - email: 'user@example.com', - emailVerified: true, - provider: 'local', - }); - - await User.create({ - _id: authorId, - email: 'author@example.com', - emailVerified: true, - provider: 'local', - }); - - // Create agent with files - const agent = await createAgent({ - id: agentId, - name: 'View-Only Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: fileIds, - }, - }, - }); - - // Grant only VIEW permission to user on the agent - await grantPermission({ - principalType: PrincipalType.USER, - principalId: userId, - resourceType: ResourceType.AGENT, - resourceId: agent._id, - accessRoleId: AccessRoleIds.AGENT_VIEWER, - grantedBy: authorId, - }); - - // Check access for files - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMap = await hasAccessToFilesViaAgent({ - userId: userId, - role: SystemRoles.USER, - fileIds, - agentId, - }); - - expect(accessMap.get(fileIds[0])).toBe(true); - expect(accessMap.get(fileIds[1])).toBe(true); - }); - }); - - describe('getFiles with agent access control', () => { - test('should return files owned by user and files accessible through agent', async () => { - const authorId = new mongoose.Types.ObjectId(); - const userId = new mongoose.Types.ObjectId(); - const agentId = `agent_${uuidv4()}`; - const ownedFileId = `file_${uuidv4()}`; - const sharedFileId = `file_${uuidv4()}`; - const inaccessibleFileId = `file_${uuidv4()}`; - - // Create users - await User.create({ - _id: userId, - email: 'user@example.com', - emailVerified: true, - provider: 'local', - }); - - await User.create({ - _id: authorId, - email: 'author@example.com', - emailVerified: true, - provider: 'local', - }); - - // Create agent with shared file - const agent = await createAgent({ - id: agentId, - name: 'Shared Agent', - provider: 'test', - model: 'test-model', - author: authorId, - tool_resources: { - file_search: { - file_ids: [sharedFileId], - }, - }, - }); - - // Grant EDIT permission to user on the agent - await grantPermission({ - principalType: PrincipalType.USER, - principalId: userId, - resourceType: ResourceType.AGENT, - resourceId: agent._id, - accessRoleId: AccessRoleIds.AGENT_EDITOR, - grantedBy: authorId, - }); - - // Create files - await createFile({ - file_id: ownedFileId, - user: userId, - filename: 'owned.txt', - filepath: '/uploads/owned.txt', - type: 'text/plain', - bytes: 100, - }); - - await createFile({ - file_id: sharedFileId, - user: authorId, - filename: 'shared.txt', - filepath: '/uploads/shared.txt', - type: 'text/plain', - bytes: 200, - embedded: true, - }); - - await createFile({ - file_id: inaccessibleFileId, - user: authorId, - filename: 'inaccessible.txt', - filepath: '/uploads/inaccessible.txt', - type: 'text/plain', - bytes: 300, - }); - - // Get all files first - const allFiles = await getFiles( - { file_id: { $in: [ownedFileId, sharedFileId, inaccessibleFileId] } }, - null, - { text: 0 }, - ); - - // Then filter by access control - const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); - const files = await filterFilesByAgentAccess({ - files: allFiles, - userId: userId, - role: SystemRoles.USER, - agentId, - }); - - expect(files).toHaveLength(2); - expect(files.map((f) => f.file_id)).toContain(ownedFileId); - expect(files.map((f) => f.file_id)).toContain(sharedFileId); - expect(files.map((f) => f.file_id)).not.toContain(inaccessibleFileId); - }); - - test('should return all files when no userId/agentId provided', async () => { - const userId = new mongoose.Types.ObjectId(); - const fileId1 = `file_${uuidv4()}`; - const fileId2 = `file_${uuidv4()}`; - - await createFile({ - file_id: fileId1, - user: userId, - filename: 'file1.txt', - filepath: '/uploads/file1.txt', - type: 'text/plain', - bytes: 100, - }); - - await createFile({ - file_id: fileId2, - user: new mongoose.Types.ObjectId(), - filename: 'file2.txt', - filepath: '/uploads/file2.txt', - type: 'text/plain', - bytes: 200, - }); - - const files = await getFiles({ file_id: { $in: [fileId1, fileId2] } }); - expect(files).toHaveLength(2); - }); - }); - - describe('Role-based file permissions', () => { - it('should optimize permission checks when role is provided', async () => { - const userId = new mongoose.Types.ObjectId(); - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileIds = [uuidv4(), uuidv4()]; - - // Create users - await User.create({ - _id: userId, - email: 'user@example.com', - emailVerified: true, - provider: 'local', - role: 'ADMIN', // User has ADMIN role - }); - - await User.create({ - _id: authorId, - email: 'author@example.com', - emailVerified: true, - provider: 'local', - }); - - // Create files - for (const fileId of fileIds) { - await createFile({ - file_id: fileId, - user: authorId, - filename: `${fileId}.txt`, - filepath: `/uploads/${fileId}.txt`, - type: 'text/plain', - bytes: 100, - }); - } - - // Create agent with files - const agent = await createAgent({ - id: agentId, - name: 'Test Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: fileIds, - }, - }, - }); - - // Grant permission to ADMIN role - await grantPermission({ - principalType: PrincipalType.ROLE, - principalId: 'ADMIN', - resourceType: ResourceType.AGENT, - resourceId: agent._id, - accessRoleId: AccessRoleIds.AGENT_EDITOR, - grantedBy: authorId, - }); - - // Check access with role provided (should avoid DB query) - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - const accessMapWithRole = await hasAccessToFilesViaAgent({ - userId: userId, - role: 'ADMIN', - fileIds, - agentId: agent.id, - }); - - // User should have access through their ADMIN role - expect(accessMapWithRole.get(fileIds[0])).toBe(true); - expect(accessMapWithRole.get(fileIds[1])).toBe(true); - - // Check access without role (will query DB to get user's role) - const accessMapWithoutRole = await hasAccessToFilesViaAgent({ - userId: userId, - fileIds, - agentId: agent.id, - }); - - // Should have same result - expect(accessMapWithoutRole.get(fileIds[0])).toBe(true); - expect(accessMapWithoutRole.get(fileIds[1])).toBe(true); - }); - - it('should deny access when user role changes', async () => { - const userId = new mongoose.Types.ObjectId(); - const authorId = new mongoose.Types.ObjectId(); - const agentId = uuidv4(); - const fileId = uuidv4(); - - // Create users - await User.create({ - _id: userId, - email: 'user@example.com', - emailVerified: true, - provider: 'local', - role: 'EDITOR', - }); - - await User.create({ - _id: authorId, - email: 'author@example.com', - emailVerified: true, - provider: 'local', - }); - - // Create file - await createFile({ - file_id: fileId, - user: authorId, - filename: 'test.txt', - filepath: '/uploads/test.txt', - type: 'text/plain', - bytes: 100, - }); - - // Create agent - const agent = await createAgent({ - id: agentId, - name: 'Test Agent', - author: authorId, - model: 'gpt-4', - provider: 'openai', - tool_resources: { - file_search: { - file_ids: [fileId], - }, - }, - }); - - // Grant permission to EDITOR role only - await grantPermission({ - principalType: PrincipalType.ROLE, - principalId: 'EDITOR', - resourceType: ResourceType.AGENT, - resourceId: agent._id, - accessRoleId: AccessRoleIds.AGENT_EDITOR, - grantedBy: authorId, - }); - - const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); - - // Check with EDITOR role - should have access - const accessAsEditor = await hasAccessToFilesViaAgent({ - userId: userId, - role: 'EDITOR', - fileIds: [fileId], - agentId: agent.id, - }); - expect(accessAsEditor.get(fileId)).toBe(true); - - // Simulate role change to USER - should lose access - const accessAsUser = await hasAccessToFilesViaAgent({ - userId: userId, - role: SystemRoles.USER, - fileIds: [fileId], - agentId: agent.id, - }); - expect(accessAsUser.get(fileId)).toBe(false); - }); - }); -}); diff --git a/api/models/Message.js b/api/models/Message.js deleted file mode 100644 index 8fe04f6f54..0000000000 --- a/api/models/Message.js +++ /dev/null @@ -1,372 +0,0 @@ -const { z } = require('zod'); -const { logger } = require('@librechat/data-schemas'); -const { createTempChatExpirationDate } = require('@librechat/api'); -const { Message } = require('~/db/models'); - -const idSchema = z.string().uuid(); - -/** - * Saves a message in the database. - * - * @async - * @function saveMessage - * @param {ServerRequest} req - The request object containing user information. - * @param {Object} params - The message data object. - * @param {string} params.endpoint - The endpoint where the message originated. - * @param {string} params.iconURL - The URL of the sender's icon. - * @param {string} params.messageId - The unique identifier for the message. - * @param {string} params.newMessageId - The new unique identifier for the message (if applicable). - * @param {string} params.conversationId - The identifier of the conversation. - * @param {string} [params.parentMessageId] - The identifier of the parent message, if any. - * @param {string} params.sender - The identifier of the sender. - * @param {string} params.text - The text content of the message. - * @param {boolean} params.isCreatedByUser - Indicates if the message was created by the user. - * @param {string} [params.error] - Any error associated with the message. - * @param {boolean} [params.unfinished] - Indicates if the message is unfinished. - * @param {Object[]} [params.files] - An array of files associated with the message. - * @param {string} [params.finish_reason] - Reason for finishing the message. - * @param {number} [params.tokenCount] - The number of tokens in the message. - * @param {string} [params.plugin] - Plugin associated with the message. - * @param {string[]} [params.plugins] - An array of plugins associated with the message. - * @param {string} [params.model] - The model used to generate the message. - * @param {Object} [metadata] - Additional metadata for this operation - * @param {string} [metadata.context] - The context of the operation - * @returns {Promise} The updated or newly inserted message document. - * @throws {Error} If there is an error in saving the message. - */ -async function saveMessage(req, params, metadata) { - if (!req?.user?.id) { - throw new Error('User not authenticated'); - } - - const validConvoId = idSchema.safeParse(params.conversationId); - if (!validConvoId.success) { - logger.warn(`Invalid conversation ID: ${params.conversationId}`); - logger.info(`---\`saveMessage\` context: ${metadata?.context}`); - logger.info(`---Invalid conversation ID Params: ${JSON.stringify(params, null, 2)}`); - return; - } - - try { - const update = { - ...params, - user: req.user.id, - messageId: params.newMessageId || params.messageId, - }; - - if (req?.body?.isTemporary) { - try { - const appConfig = req.config; - update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig); - } catch (err) { - logger.error('Error creating temporary chat expiration date:', err); - logger.info(`---\`saveMessage\` context: ${metadata?.context}`); - update.expiredAt = null; - } - } else { - update.expiredAt = null; - } - - if (update.tokenCount != null && isNaN(update.tokenCount)) { - logger.warn( - `Resetting invalid \`tokenCount\` for message \`${params.messageId}\`: ${update.tokenCount}`, - ); - logger.info(`---\`saveMessage\` context: ${metadata?.context}`); - update.tokenCount = 0; - } - const message = await Message.findOneAndUpdate( - { messageId: params.messageId, user: req.user.id }, - update, - { upsert: true, new: true }, - ); - - return message.toObject(); - } catch (err) { - logger.error('Error saving message:', err); - logger.info(`---\`saveMessage\` context: ${metadata?.context}`); - - // Check if this is a duplicate key error (MongoDB error code 11000) - if (err.code === 11000 && err.message.includes('duplicate key error')) { - // Log the duplicate key error but don't crash the application - logger.warn(`Duplicate messageId detected: ${params.messageId}. Continuing execution.`); - - try { - // Try to find the existing message with this ID - const existingMessage = await Message.findOne({ - messageId: params.messageId, - user: req.user.id, - }); - - // If we found it, return it - if (existingMessage) { - return existingMessage.toObject(); - } - - // If we can't find it (unlikely but possible in race conditions) - return { - ...params, - messageId: params.messageId, - user: req.user.id, - }; - } catch (findError) { - // If the findOne also fails, log it but don't crash - logger.warn( - `Could not retrieve existing message with ID ${params.messageId}: ${findError.message}`, - ); - return { - ...params, - messageId: params.messageId, - user: req.user.id, - }; - } - } - - throw err; // Re-throw other errors - } -} - -/** - * Saves multiple messages in the database in bulk. - * - * @async - * @function bulkSaveMessages - * @param {Object[]} messages - An array of message objects to save. - * @param {boolean} [overrideTimestamp=false] - Indicates whether to override the timestamps of the messages. Defaults to false. - * @returns {Promise} The result of the bulk write operation. - * @throws {Error} If there is an error in saving messages in bulk. - */ -async function bulkSaveMessages(messages, overrideTimestamp = false) { - try { - const bulkOps = messages.map((message) => ({ - updateOne: { - filter: { messageId: message.messageId }, - update: message, - timestamps: !overrideTimestamp, - upsert: true, - }, - })); - const result = await Message.bulkWrite(bulkOps); - return result; - } catch (err) { - logger.error('Error saving messages in bulk:', err); - throw err; - } -} - -/** - * Records a message in the database. - * - * @async - * @function recordMessage - * @param {Object} params - The message data object. - * @param {string} params.user - The identifier of the user. - * @param {string} params.endpoint - The endpoint where the message originated. - * @param {string} params.messageId - The unique identifier for the message. - * @param {string} params.conversationId - The identifier of the conversation. - * @param {string} [params.parentMessageId] - The identifier of the parent message, if any. - * @param {Partial} rest - Any additional properties from the TMessage typedef not explicitly listed. - * @returns {Promise} The updated or newly inserted message document. - * @throws {Error} If there is an error in saving the message. - */ -async function recordMessage({ - user, - endpoint, - messageId, - conversationId, - parentMessageId, - ...rest -}) { - try { - // No parsing of convoId as may use threadId - const message = { - user, - endpoint, - messageId, - conversationId, - parentMessageId, - ...rest, - }; - - return await Message.findOneAndUpdate({ user, messageId }, message, { - upsert: true, - new: true, - }); - } catch (err) { - logger.error('Error recording message:', err); - throw err; - } -} - -/** - * Updates the text of a message. - * - * @async - * @function updateMessageText - * @param {Object} params - The update data object. - * @param {Object} req - The request object. - * @param {string} params.messageId - The unique identifier for the message. - * @param {string} params.text - The new text content of the message. - * @returns {Promise} - * @throws {Error} If there is an error in updating the message text. - */ -async function updateMessageText(req, { messageId, text }) { - try { - await Message.updateOne({ messageId, user: req.user.id }, { text }); - } catch (err) { - logger.error('Error updating message text:', err); - throw err; - } -} - -/** - * Updates a message. - * - * @async - * @function updateMessage - * @param {Object} req - The request object. - * @param {Object} message - The message object containing update data. - * @param {string} message.messageId - The unique identifier for the message. - * @param {string} [message.text] - The new text content of the message. - * @param {Object[]} [message.files] - The files associated with the message. - * @param {boolean} [message.isCreatedByUser] - Indicates if the message was created by the user. - * @param {string} [message.sender] - The identifier of the sender. - * @param {number} [message.tokenCount] - The number of tokens in the message. - * @param {Object} [metadata] - The operation metadata - * @param {string} [metadata.context] - The operation metadata - * @returns {Promise} The updated message document. - * @throws {Error} If there is an error in updating the message or if the message is not found. - */ -async function updateMessage(req, message, metadata) { - try { - const { messageId, ...update } = message; - const updatedMessage = await Message.findOneAndUpdate( - { messageId, user: req.user.id }, - update, - { - new: true, - }, - ); - - if (!updatedMessage) { - throw new Error('Message not found or user not authorized.'); - } - - return { - messageId: updatedMessage.messageId, - conversationId: updatedMessage.conversationId, - parentMessageId: updatedMessage.parentMessageId, - sender: updatedMessage.sender, - text: updatedMessage.text, - isCreatedByUser: updatedMessage.isCreatedByUser, - tokenCount: updatedMessage.tokenCount, - feedback: updatedMessage.feedback, - }; - } catch (err) { - logger.error('Error updating message:', err); - if (metadata && metadata?.context) { - logger.info(`---\`updateMessage\` context: ${metadata.context}`); - } - throw err; - } -} - -/** - * Deletes messages in a conversation since a specific message. - * - * @async - * @function deleteMessagesSince - * @param {Object} params - The parameters object. - * @param {Object} req - The request object. - * @param {string} params.messageId - The unique identifier for the message. - * @param {string} params.conversationId - The identifier of the conversation. - * @returns {Promise} The number of deleted messages. - * @throws {Error} If there is an error in deleting messages. - */ -async function deleteMessagesSince(req, { messageId, conversationId }) { - try { - const message = await Message.findOne({ messageId, user: req.user.id }).lean(); - - if (message) { - const query = Message.find({ conversationId, user: req.user.id }); - return await query.deleteMany({ - createdAt: { $gt: message.createdAt }, - }); - } - return undefined; - } catch (err) { - logger.error('Error deleting messages:', err); - throw err; - } -} - -/** - * Retrieves messages from the database. - * @async - * @function getMessages - * @param {Record} filter - The filter criteria. - * @param {string | undefined} [select] - The fields to select. - * @returns {Promise} The messages that match the filter criteria. - * @throws {Error} If there is an error in retrieving messages. - */ -async function getMessages(filter, select) { - try { - if (select) { - return await Message.find(filter).select(select).sort({ createdAt: 1 }).lean(); - } - - return await Message.find(filter).sort({ createdAt: 1 }).lean(); - } catch (err) { - logger.error('Error getting messages:', err); - throw err; - } -} - -/** - * Retrieves a single message from the database. - * @async - * @function getMessage - * @param {{ user: string, messageId: string }} params - The search parameters - * @returns {Promise} The message that matches the criteria or null if not found - * @throws {Error} If there is an error in retrieving the message - */ -async function getMessage({ user, messageId }) { - try { - return await Message.findOne({ - user, - messageId, - }).lean(); - } catch (err) { - logger.error('Error getting message:', err); - throw err; - } -} - -/** - * Deletes messages from the database. - * - * @async - * @function deleteMessages - * @param {import('mongoose').FilterQuery} filter - The filter criteria to find messages to delete. - * @returns {Promise} The metadata with count of deleted messages. - * @throws {Error} If there is an error in deleting messages. - */ -async function deleteMessages(filter) { - try { - return await Message.deleteMany(filter); - } catch (err) { - logger.error('Error deleting messages:', err); - throw err; - } -} - -module.exports = { - saveMessage, - bulkSaveMessages, - recordMessage, - updateMessageText, - updateMessage, - deleteMessagesSince, - getMessages, - getMessage, - deleteMessages, -}; diff --git a/api/models/Preset.js b/api/models/Preset.js deleted file mode 100644 index 4db3d59066..0000000000 --- a/api/models/Preset.js +++ /dev/null @@ -1,82 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { Preset } = require('~/db/models'); - -const getPreset = async (user, presetId) => { - try { - return await Preset.findOne({ user, presetId }).lean(); - } catch (error) { - logger.error('[getPreset] Error getting single preset', error); - return { message: 'Error getting single preset' }; - } -}; - -module.exports = { - getPreset, - getPresets: async (user, filter) => { - try { - const presets = await Preset.find({ ...filter, user }).lean(); - const defaultValue = 10000; - - presets.sort((a, b) => { - let orderA = a.order !== undefined ? a.order : defaultValue; - let orderB = b.order !== undefined ? b.order : defaultValue; - - if (orderA !== orderB) { - return orderA - orderB; - } - - return b.updatedAt - a.updatedAt; - }); - - return presets; - } catch (error) { - logger.error('[getPresets] Error getting presets', error); - return { message: 'Error retrieving presets' }; - } - }, - savePreset: async (user, { presetId, newPresetId, defaultPreset, ...preset }) => { - try { - const setter = { $set: {} }; - const { user: _, ...cleanPreset } = preset; - const update = { presetId, ...cleanPreset }; - if (preset.tools && Array.isArray(preset.tools)) { - update.tools = - preset.tools - .map((tool) => tool?.pluginKey ?? tool) - .filter((toolName) => typeof toolName === 'string') ?? []; - } - if (newPresetId) { - update.presetId = newPresetId; - } - - if (defaultPreset) { - update.defaultPreset = defaultPreset; - update.order = 0; - - const currentDefault = await Preset.findOne({ defaultPreset: true, user }); - - if (currentDefault && currentDefault.presetId !== presetId) { - await Preset.findByIdAndUpdate(currentDefault._id, { - $unset: { defaultPreset: '', order: '' }, - }); - } - } else if (defaultPreset === false) { - update.defaultPreset = undefined; - update.order = undefined; - setter['$unset'] = { defaultPreset: '', order: '' }; - } - - setter.$set = update; - return await Preset.findOneAndUpdate({ presetId, user }, setter, { new: true, upsert: true }); - } catch (error) { - logger.error('[savePreset] Error saving preset', error); - return { message: 'Error saving preset' }; - } - }, - deletePresets: async (user, filter) => { - // let toRemove = await Preset.find({ ...filter, user }).select('presetId'); - // const ids = toRemove.map((instance) => instance.presetId); - let deleteCount = await Preset.deleteMany({ ...filter, user }); - return deleteCount; - }, -}; diff --git a/api/models/Prompt.js b/api/models/Prompt.js deleted file mode 100644 index 38d56b53a4..0000000000 --- a/api/models/Prompt.js +++ /dev/null @@ -1,588 +0,0 @@ -const { ObjectId } = require('mongodb'); -const { escapeRegExp } = require('@librechat/api'); -const { logger } = require('@librechat/data-schemas'); -const { SystemRoles, ResourceType, SystemCategories } = require('librechat-data-provider'); -const { - getSoleOwnedResourceIds, - removeAllPermissions, -} = require('~/server/services/PermissionService'); -const { PromptGroup, Prompt, AclEntry } = require('~/db/models'); - -/** - * Batch-fetches production prompts for an array of prompt groups - * and attaches them as `productionPrompt` field. - * Replaces $lookup aggregation for FerretDB compatibility. - */ -const attachProductionPrompts = async (groups) => { - const uniqueIds = [...new Set(groups.map((g) => g.productionId?.toString()).filter(Boolean))]; - if (uniqueIds.length === 0) { - return groups.map((g) => ({ ...g, productionPrompt: null })); - } - - const prompts = await Prompt.find({ _id: { $in: uniqueIds } }) - .select('prompt') - .lean(); - const promptMap = new Map(prompts.map((p) => [p._id.toString(), p])); - - return groups.map((g) => ({ - ...g, - productionPrompt: g.productionId ? (promptMap.get(g.productionId.toString()) ?? null) : null, - })); -}; - -/** - * Get all prompt groups with filters - * @param {ServerRequest} req - * @param {TPromptGroupsWithFilterRequest} filter - * @returns {Promise} - */ -const getAllPromptGroups = async (req, filter) => { - try { - const { name, ...query } = filter; - - if (name) { - query.name = new RegExp(escapeRegExp(name), 'i'); - } - if (!query.category) { - delete query.category; - } else if (query.category === SystemCategories.MY_PROMPTS) { - delete query.category; - } else if (query.category === SystemCategories.NO_CATEGORY) { - query.category = ''; - } else if (query.category === SystemCategories.SHARED_PROMPTS) { - delete query.category; - } - - let combinedQuery = query; - - const groups = await PromptGroup.find(combinedQuery) - .sort({ createdAt: -1 }) - .select('name oneliner category author authorName createdAt updatedAt command productionId') - .lean(); - return await attachProductionPrompts(groups); - } catch (error) { - console.error('Error getting all prompt groups', error); - return { message: 'Error getting all prompt groups' }; - } -}; - -/** - * Get prompt groups with filters - * @param {ServerRequest} req - * @param {TPromptGroupsWithFilterRequest} filter - * @returns {Promise} - */ -const getPromptGroups = async (req, filter) => { - try { - const { pageNumber = 1, pageSize = 10, name, ...query } = filter; - - const validatedPageNumber = Math.max(parseInt(pageNumber, 10), 1); - const validatedPageSize = Math.max(parseInt(pageSize, 10), 1); - - if (name) { - query.name = new RegExp(escapeRegExp(name), 'i'); - } - if (!query.category) { - delete query.category; - } else if (query.category === SystemCategories.MY_PROMPTS) { - delete query.category; - } else if (query.category === SystemCategories.NO_CATEGORY) { - query.category = ''; - } else if (query.category === SystemCategories.SHARED_PROMPTS) { - delete query.category; - } - - let combinedQuery = query; - - const skip = (validatedPageNumber - 1) * validatedPageSize; - const limit = validatedPageSize; - - const [groups, totalPromptGroups] = await Promise.all([ - PromptGroup.find(combinedQuery) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit) - .select( - 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', - ) - .lean(), - PromptGroup.countDocuments(combinedQuery), - ]); - - const promptGroups = await attachProductionPrompts(groups); - - return { - promptGroups, - pageNumber: validatedPageNumber.toString(), - pageSize: validatedPageSize.toString(), - pages: Math.ceil(totalPromptGroups / validatedPageSize).toString(), - }; - } catch (error) { - console.error('Error getting prompt groups', error); - return { message: 'Error getting prompt groups' }; - } -}; - -/** - * @param {Object} fields - * @param {string} fields._id - * @param {string} fields.author - * @param {string} fields.role - * @returns {Promise} - */ -const deletePromptGroup = async ({ _id, author, role }) => { - // Build query - with ACL, author is optional - const query = { _id }; - const groupQuery = { groupId: new ObjectId(_id) }; - - // Legacy: Add author filter if provided (backward compatibility) - if (author && role !== SystemRoles.ADMIN) { - query.author = author; - groupQuery.author = author; - } - - const response = await PromptGroup.deleteOne(query); - - if (!response || response.deletedCount === 0) { - throw new Error('Prompt group not found'); - } - - await Prompt.deleteMany(groupQuery); - - try { - await removeAllPermissions({ resourceType: ResourceType.PROMPTGROUP, resourceId: _id }); - } catch (error) { - logger.error('Error removing promptGroup permissions:', error); - } - - return { message: 'Prompt group deleted successfully' }; -}; - -/** - * Get prompt groups by accessible IDs with optional cursor-based pagination. - * @param {Object} params - The parameters for getting accessible prompt groups. - * @param {Array} [params.accessibleIds] - Array of prompt group ObjectIds the user has ACL access to. - * @param {Object} [params.otherParams] - Additional query parameters (including author filter). - * @param {number} [params.limit] - Number of prompt groups to return (max 100). If not provided, returns all prompt groups. - * @param {string} [params.after] - Cursor for pagination - get prompt groups after this cursor. // base64 encoded JSON string with updatedAt and _id. - * @returns {Promise} A promise that resolves to an object containing the prompt groups data and pagination info. - */ -async function getListPromptGroupsByAccess({ - accessibleIds = [], - otherParams = {}, - limit = null, - after = null, -}) { - const isPaginated = limit !== null && limit !== undefined; - const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null; - - const baseQuery = { ...otherParams, _id: { $in: accessibleIds } }; - - if (after && typeof after === 'string' && after !== 'undefined' && after !== 'null') { - try { - const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8')); - const { updatedAt, _id } = cursor; - - const cursorCondition = { - $or: [ - { updatedAt: { $lt: new Date(updatedAt) } }, - { updatedAt: new Date(updatedAt), _id: { $gt: new ObjectId(_id) } }, - ], - }; - - if (Object.keys(baseQuery).length > 0) { - baseQuery.$and = [{ ...baseQuery }, cursorCondition]; - Object.keys(baseQuery).forEach((key) => { - if (key !== '$and') delete baseQuery[key]; - }); - } else { - Object.assign(baseQuery, cursorCondition); - } - } catch (error) { - logger.warn('Invalid cursor:', error.message); - } - } - - const findQuery = PromptGroup.find(baseQuery) - .sort({ updatedAt: -1, _id: 1 }) - .select( - 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', - ); - - if (isPaginated) { - findQuery.limit(normalizedLimit + 1); - } - - const groups = await findQuery.lean(); - const promptGroups = await attachProductionPrompts(groups); - - const hasMore = isPaginated ? promptGroups.length > normalizedLimit : false; - const data = (isPaginated ? promptGroups.slice(0, normalizedLimit) : promptGroups).map( - (group) => { - if (group.author) { - group.author = group.author.toString(); - } - return group; - }, - ); - - let nextCursor = null; - if (isPaginated && hasMore && data.length > 0) { - const lastGroup = promptGroups[normalizedLimit - 1]; - nextCursor = Buffer.from( - JSON.stringify({ - updatedAt: lastGroup.updatedAt.toISOString(), - _id: lastGroup._id.toString(), - }), - ).toString('base64'); - } - - return { - object: 'list', - data, - first_id: data.length > 0 ? data[0]._id.toString() : null, - last_id: data.length > 0 ? data[data.length - 1]._id.toString() : null, - has_more: hasMore, - after: nextCursor, - }; -} - -module.exports = { - getPromptGroups, - deletePromptGroup, - getAllPromptGroups, - getListPromptGroupsByAccess, - /** - * Create a prompt and its respective group - * @param {TCreatePromptRecord} saveData - * @returns {Promise} - */ - createPromptGroup: async (saveData) => { - try { - const { prompt, group, author, authorName } = saveData; - - let newPromptGroup = await PromptGroup.findOneAndUpdate( - { ...group, author, authorName, productionId: null }, - { $setOnInsert: { ...group, author, authorName, productionId: null } }, - { new: true, upsert: true }, - ) - .lean() - .select('-__v') - .exec(); - - const newPrompt = await Prompt.findOneAndUpdate( - { ...prompt, author, groupId: newPromptGroup._id }, - { $setOnInsert: { ...prompt, author, groupId: newPromptGroup._id } }, - { new: true, upsert: true }, - ) - .lean() - .select('-__v') - .exec(); - - newPromptGroup = await PromptGroup.findByIdAndUpdate( - newPromptGroup._id, - { productionId: newPrompt._id }, - { new: true }, - ) - .lean() - .select('-__v') - .exec(); - - return { - prompt: newPrompt, - group: { - ...newPromptGroup, - productionPrompt: { prompt: newPrompt.prompt }, - }, - }; - } catch (error) { - logger.error('Error saving prompt group', error); - throw new Error('Error saving prompt group'); - } - }, - /** - * Save a prompt - * @param {TCreatePromptRecord} saveData - * @returns {Promise} - */ - savePrompt: async (saveData) => { - try { - const { prompt, author } = saveData; - const newPromptData = { - ...prompt, - author, - }; - - /** @type {TPrompt} */ - let newPrompt; - try { - newPrompt = await Prompt.create(newPromptData); - } catch (error) { - if (error?.message?.includes('groupId_1_version_1')) { - await Prompt.db.collection('prompts').dropIndex('groupId_1_version_1'); - } else { - throw error; - } - newPrompt = await Prompt.create(newPromptData); - } - - return { prompt: newPrompt }; - } catch (error) { - logger.error('Error saving prompt', error); - return { message: 'Error saving prompt' }; - } - }, - getPrompts: async (filter) => { - try { - return await Prompt.find(filter).sort({ createdAt: -1 }).lean(); - } catch (error) { - logger.error('Error getting prompts', error); - return { message: 'Error getting prompts' }; - } - }, - getPrompt: async (filter) => { - try { - if (filter.groupId) { - filter.groupId = new ObjectId(filter.groupId); - } - return await Prompt.findOne(filter).lean(); - } catch (error) { - logger.error('Error getting prompt', error); - return { message: 'Error getting prompt' }; - } - }, - /** - * Get prompt groups with filters - * @param {TGetRandomPromptsRequest} filter - * @returns {Promise} - */ - getRandomPromptGroups: async (filter) => { - try { - const categories = await PromptGroup.distinct('category', { category: { $ne: '' } }); - - for (let i = categories.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [categories[i], categories[j]] = [categories[j], categories[i]]; - } - - const skip = +filter.skip; - const limit = +filter.limit; - const selectedCategories = categories.slice(skip, skip + limit); - - if (selectedCategories.length === 0) { - return { prompts: [] }; - } - - const groups = await PromptGroup.find({ category: { $in: selectedCategories } }).lean(); - - const groupByCategory = new Map(); - for (const group of groups) { - if (!groupByCategory.has(group.category)) { - groupByCategory.set(group.category, group); - } - } - - const prompts = selectedCategories.map((cat) => groupByCategory.get(cat)).filter(Boolean); - - return { prompts }; - } catch (error) { - logger.error('Error getting prompt groups', error); - return { message: 'Error getting prompt groups' }; - } - }, - getPromptGroupsWithPrompts: async (filter) => { - try { - return await PromptGroup.findOne(filter) - .populate({ - path: 'prompts', - select: '-_id -__v -user', - }) - .select('-_id -__v -user') - .lean(); - } catch (error) { - logger.error('Error getting prompt groups', error); - return { message: 'Error getting prompt groups' }; - } - }, - getPromptGroup: async (filter) => { - try { - return await PromptGroup.findOne(filter).lean(); - } catch (error) { - logger.error('Error getting prompt group', error); - return { message: 'Error getting prompt group' }; - } - }, - /** - * Deletes a prompt and its corresponding prompt group if it is the last prompt in the group. - * - * @param {Object} options - The options for deleting the prompt. - * @param {ObjectId|string} options.promptId - The ID of the prompt to delete. - * @param {ObjectId|string} options.groupId - The ID of the prompt's group. - * @param {ObjectId|string} options.author - The ID of the prompt's author. - * @param {string} options.role - The role of the prompt's author. - * @return {Promise} An object containing the result of the deletion. - * If the prompt was deleted successfully, the object will have a property 'prompt' with the value 'Prompt deleted successfully'. - * If the prompt group was deleted successfully, the object will have a property 'promptGroup' with the message 'Prompt group deleted successfully' and id of the deleted group. - * If there was an error deleting the prompt, the object will have a property 'message' with the value 'Error deleting prompt'. - */ - deletePrompt: async ({ promptId, groupId, author, role }) => { - const query = { _id: promptId, groupId, author }; - if (role === SystemRoles.ADMIN) { - delete query.author; - } - const { deletedCount } = await Prompt.deleteOne(query); - if (deletedCount === 0) { - throw new Error('Failed to delete the prompt'); - } - - const remainingPrompts = await Prompt.find({ groupId }) - .select('_id') - .sort({ createdAt: 1 }) - .lean(); - - if (remainingPrompts.length === 0) { - // Remove all ACL entries for the promptGroup when deleting the last prompt - try { - await removeAllPermissions({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: groupId, - }); - } catch (error) { - logger.error('Error removing promptGroup permissions:', error); - } - - await PromptGroup.deleteOne({ _id: groupId }); - - return { - prompt: 'Prompt deleted successfully', - promptGroup: { - message: 'Prompt group deleted successfully', - id: groupId, - }, - }; - } else { - const promptGroup = await PromptGroup.findById(groupId).lean(); - if (promptGroup.productionId.toString() === promptId.toString()) { - await PromptGroup.updateOne( - { _id: groupId }, - { productionId: remainingPrompts[remainingPrompts.length - 1]._id }, - ); - } - - return { prompt: 'Prompt deleted successfully' }; - } - }, - /** - * Delete prompt groups solely owned by the user and clean up their prompts/ACLs. - * Groups with other owners are left intact; the caller is responsible for - * removing the user's own ACL principal entries separately. - * - * Also handles legacy (pre-ACL) prompt groups that only have the author field set, - * ensuring they are not orphaned if the permission migration has not been run. - * @param {string} userId - The ID of the user whose prompts and prompt groups are to be deleted. - */ - deleteUserPrompts: async (userId) => { - try { - const userObjectId = new ObjectId(userId); - const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.PROMPTGROUP); - - const authoredGroups = await PromptGroup.find({ author: userObjectId }).select('_id').lean(); - const authoredGroupIds = authoredGroups.map((g) => g._id); - - const migratedEntries = - authoredGroupIds.length > 0 - ? await AclEntry.find({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: { $in: authoredGroupIds }, - }) - .select('resourceId') - .lean() - : []; - const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString())); - const legacyGroupIds = authoredGroupIds.filter((id) => !migratedIds.has(id.toString())); - - const allGroupIdsToDelete = [...soleOwnedIds, ...legacyGroupIds]; - - if (allGroupIdsToDelete.length === 0) { - return; - } - - await AclEntry.deleteMany({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: { $in: allGroupIdsToDelete }, - }); - - await PromptGroup.deleteMany({ _id: { $in: allGroupIdsToDelete } }); - await Prompt.deleteMany({ groupId: { $in: allGroupIdsToDelete } }); - } catch (error) { - logger.error('[deleteUserPrompts] General error:', error); - } - }, - /** - * Update prompt group - * @param {Partial} filter - Filter to find prompt group - * @param {Partial} data - Data to update - * @returns {Promise} - */ - updatePromptGroup: async (filter, data) => { - try { - const updateOps = {}; - - const updateData = { ...data, ...updateOps }; - const updatedDoc = await PromptGroup.findOneAndUpdate(filter, updateData, { - new: true, - upsert: false, - }); - - if (!updatedDoc) { - throw new Error('Prompt group not found'); - } - - return updatedDoc; - } catch (error) { - logger.error('Error updating prompt group', error); - return { message: 'Error updating prompt group' }; - } - }, - /** - * Function to make a prompt production based on its ID. - * @param {String} promptId - The ID of the prompt to make production. - * @returns {Object} The result of the production operation. - */ - makePromptProduction: async (promptId) => { - try { - const prompt = await Prompt.findById(promptId).lean(); - - if (!prompt) { - throw new Error('Prompt not found'); - } - - await PromptGroup.findByIdAndUpdate( - prompt.groupId, - { productionId: prompt._id }, - { new: true }, - ) - .lean() - .exec(); - - return { - message: 'Prompt production made successfully', - }; - } catch (error) { - logger.error('Error making prompt production', error); - return { message: 'Error making prompt production' }; - } - }, - updatePromptLabels: async (_id, labels) => { - try { - const response = await Prompt.updateOne({ _id }, { $set: { labels } }); - if (response.matchedCount === 0) { - return { message: 'Prompt not found' }; - } - return { message: 'Prompt labels updated successfully' }; - } catch (error) { - logger.error('Error updating prompt labels', error); - return { message: 'Error updating prompt labels' }; - } - }, -}; diff --git a/api/models/Prompt.spec.js b/api/models/Prompt.spec.js deleted file mode 100644 index 5c1c8c8256..0000000000 --- a/api/models/Prompt.spec.js +++ /dev/null @@ -1,784 +0,0 @@ -const mongoose = require('mongoose'); -const { ObjectId } = require('mongodb'); -const { logger } = require('@librechat/data-schemas'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { - SystemRoles, - ResourceType, - AccessRoleIds, - PrincipalType, - PermissionBits, -} = require('librechat-data-provider'); - -// Mock the config/connect module to prevent connection attempts during tests -jest.mock('../../config/connect', () => jest.fn().mockResolvedValue(true)); - -const dbModels = require('~/db/models'); - -// Disable console for tests -logger.silent = true; - -let mongoServer; -let Prompt, PromptGroup, AclEntry, AccessRole, User, Group; -let promptFns, permissionService; -let testUsers, testGroups, testRoles; - -beforeAll(async () => { - // Set up MongoDB memory server - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - await mongoose.connect(mongoUri); - - // Initialize models - Prompt = dbModels.Prompt; - PromptGroup = dbModels.PromptGroup; - AclEntry = dbModels.AclEntry; - AccessRole = dbModels.AccessRole; - User = dbModels.User; - Group = dbModels.Group; - - promptFns = require('~/models/Prompt'); - permissionService = require('~/server/services/PermissionService'); - - // Create test data - await setupTestData(); -}); - -afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - jest.clearAllMocks(); -}); - -async function setupTestData() { - // Create access roles for promptGroups - testRoles = { - viewer: await AccessRole.create({ - accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, - name: 'Viewer', - description: 'Can view promptGroups', - resourceType: ResourceType.PROMPTGROUP, - permBits: PermissionBits.VIEW, - }), - editor: await AccessRole.create({ - accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, - name: 'Editor', - description: 'Can view and edit promptGroups', - resourceType: ResourceType.PROMPTGROUP, - permBits: PermissionBits.VIEW | PermissionBits.EDIT, - }), - owner: await AccessRole.create({ - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - name: 'Owner', - description: 'Full control over promptGroups', - resourceType: ResourceType.PROMPTGROUP, - permBits: - PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, - }), - }; - - // Create test users - testUsers = { - owner: await User.create({ - name: 'Prompt Owner', - email: 'owner@example.com', - role: SystemRoles.USER, - }), - editor: await User.create({ - name: 'Prompt Editor', - email: 'editor@example.com', - role: SystemRoles.USER, - }), - viewer: await User.create({ - name: 'Prompt Viewer', - email: 'viewer@example.com', - role: SystemRoles.USER, - }), - admin: await User.create({ - name: 'Admin User', - email: 'admin@example.com', - role: SystemRoles.ADMIN, - }), - noAccess: await User.create({ - name: 'No Access User', - email: 'noaccess@example.com', - role: SystemRoles.USER, - }), - }; - - // Create test groups - testGroups = { - editors: await Group.create({ - name: 'Prompt Editors', - description: 'Group with editor access', - }), - viewers: await Group.create({ - name: 'Prompt Viewers', - description: 'Group with viewer access', - }), - }; -} - -describe('Prompt ACL Permissions', () => { - describe('Creating Prompts with Permissions', () => { - it('should grant owner permissions when creating a prompt', async () => { - // First create a group - const testGroup = await PromptGroup.create({ - name: 'Test Group', - category: 'testing', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new mongoose.Types.ObjectId(), - }); - - const promptData = { - prompt: { - prompt: 'Test prompt content', - name: 'Test Prompt', - type: 'text', - groupId: testGroup._id, - }, - author: testUsers.owner._id, - }; - - await promptFns.savePrompt(promptData); - - // Manually grant permissions as would happen in the route - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: testUsers.owner._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: testUsers.owner._id, - }); - - // Check ACL entry - const aclEntry = await AclEntry.findOne({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: testGroup._id, - principalType: PrincipalType.USER, - principalId: testUsers.owner._id, - }); - - expect(aclEntry).toBeTruthy(); - expect(aclEntry.permBits).toBe(testRoles.owner.permBits); - }); - }); - - describe('Accessing Prompts', () => { - let testPromptGroup; - - beforeEach(async () => { - // Create a prompt group - testPromptGroup = await PromptGroup.create({ - name: 'Test Group', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new ObjectId(), - }); - - // Create a prompt - await Prompt.create({ - prompt: 'Test prompt for access control', - name: 'Access Test Prompt', - author: testUsers.owner._id, - groupId: testPromptGroup._id, - type: 'text', - }); - - // Grant owner permissions - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: testUsers.owner._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: testUsers.owner._id, - }); - }); - - afterEach(async () => { - await Prompt.deleteMany({}); - await PromptGroup.deleteMany({}); - await AclEntry.deleteMany({}); - }); - - it('owner should have full access to their prompt', async () => { - const hasAccess = await permissionService.checkPermission({ - userId: testUsers.owner._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.VIEW, - }); - - expect(hasAccess).toBe(true); - - const canEdit = await permissionService.checkPermission({ - userId: testUsers.owner._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.EDIT, - }); - - expect(canEdit).toBe(true); - }); - - it('user with viewer role should only have view access', async () => { - // Grant viewer permissions - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: testUsers.viewer._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, - grantedBy: testUsers.owner._id, - }); - - const canView = await permissionService.checkPermission({ - userId: testUsers.viewer._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.VIEW, - }); - - const canEdit = await permissionService.checkPermission({ - userId: testUsers.viewer._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.EDIT, - }); - - expect(canView).toBe(true); - expect(canEdit).toBe(false); - }); - - it('user without permissions should have no access', async () => { - const hasAccess = await permissionService.checkPermission({ - userId: testUsers.noAccess._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.VIEW, - }); - - expect(hasAccess).toBe(false); - }); - - it('admin should have access regardless of permissions', async () => { - // Admin users should work through normal permission system - // The middleware layer handles admin bypass, not the permission service - const hasAccess = await permissionService.checkPermission({ - userId: testUsers.admin._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.VIEW, - }); - - // Without explicit permissions, even admin won't have access at this layer - expect(hasAccess).toBe(false); - - // The actual admin bypass happens in the middleware layer (`canAccessPromptViaGroup`/`canAccessPromptGroupResource`) - // which checks req.user.role === SystemRoles.ADMIN - }); - }); - - describe('Group-based Access', () => { - let testPromptGroup; - - beforeEach(async () => { - // Create a prompt group first - testPromptGroup = await PromptGroup.create({ - name: 'Group Access Test Group', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new ObjectId(), - }); - - await Prompt.create({ - prompt: 'Group access test prompt', - name: 'Group Test', - author: testUsers.owner._id, - groupId: testPromptGroup._id, - type: 'text', - }); - - // Add users to groups - await User.findByIdAndUpdate(testUsers.editor._id, { - $push: { groups: testGroups.editors._id }, - }); - - await User.findByIdAndUpdate(testUsers.viewer._id, { - $push: { groups: testGroups.viewers._id }, - }); - }); - - afterEach(async () => { - await Prompt.deleteMany({}); - await AclEntry.deleteMany({}); - await User.updateMany({}, { $set: { groups: [] } }); - }); - - it('group members should inherit group permissions', async () => { - // Create a prompt group - const testPromptGroup = await PromptGroup.create({ - name: 'Group Test Group', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new ObjectId(), - }); - - const { addUserToGroup } = require('~/models'); - await addUserToGroup(testUsers.editor._id, testGroups.editors._id); - - const prompt = await promptFns.savePrompt({ - author: testUsers.owner._id, - prompt: { - prompt: 'Group test prompt', - name: 'Group Test', - groupId: testPromptGroup._id, - type: 'text', - }, - }); - - // Check if savePrompt returned an error - if (!prompt || !prompt.prompt) { - throw new Error(`Failed to save prompt: ${prompt?.message || 'Unknown error'}`); - } - - // Grant edit permissions to the group - await permissionService.grantPermission({ - principalType: PrincipalType.GROUP, - principalId: testGroups.editors._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, - grantedBy: testUsers.owner._id, - }); - - // Check if group member has access - const hasAccess = await permissionService.checkPermission({ - userId: testUsers.editor._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.EDIT, - }); - - expect(hasAccess).toBe(true); - - // Check that non-member doesn't have access - const nonMemberAccess = await permissionService.checkPermission({ - userId: testUsers.viewer._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - requiredPermission: PermissionBits.EDIT, - }); - - expect(nonMemberAccess).toBe(false); - }); - }); - - describe('Public Access', () => { - let publicPromptGroup, privatePromptGroup; - - beforeEach(async () => { - // Create separate prompt groups for public and private access - publicPromptGroup = await PromptGroup.create({ - name: 'Public Access Test Group', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new ObjectId(), - }); - - privatePromptGroup = await PromptGroup.create({ - name: 'Private Access Test Group', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new ObjectId(), - }); - - // Create prompts in their respective groups - await Prompt.create({ - prompt: 'Public prompt', - name: 'Public', - author: testUsers.owner._id, - groupId: publicPromptGroup._id, - type: 'text', - }); - - await Prompt.create({ - prompt: 'Private prompt', - name: 'Private', - author: testUsers.owner._id, - groupId: privatePromptGroup._id, - type: 'text', - }); - - // Grant public view access to publicPromptGroup - await permissionService.grantPermission({ - principalType: PrincipalType.PUBLIC, - principalId: null, - resourceType: ResourceType.PROMPTGROUP, - resourceId: publicPromptGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, - grantedBy: testUsers.owner._id, - }); - - // Grant only owner access to privatePromptGroup - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: testUsers.owner._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: privatePromptGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: testUsers.owner._id, - }); - }); - - afterEach(async () => { - await Prompt.deleteMany({}); - await PromptGroup.deleteMany({}); - await AclEntry.deleteMany({}); - }); - - it('public prompt should be accessible to any user', async () => { - const hasAccess = await permissionService.checkPermission({ - userId: testUsers.noAccess._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: publicPromptGroup._id, - requiredPermission: PermissionBits.VIEW, - includePublic: true, - }); - - expect(hasAccess).toBe(true); - }); - - it('private prompt should not be accessible to unauthorized users', async () => { - const hasAccess = await permissionService.checkPermission({ - userId: testUsers.noAccess._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: privatePromptGroup._id, - requiredPermission: PermissionBits.VIEW, - includePublic: true, - }); - - expect(hasAccess).toBe(false); - }); - }); - - describe('Prompt Deletion', () => { - let testPromptGroup; - - it('should remove ACL entries when prompt is deleted', async () => { - testPromptGroup = await PromptGroup.create({ - name: 'Deletion Test Group', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new ObjectId(), - }); - - const prompt = await promptFns.savePrompt({ - author: testUsers.owner._id, - prompt: { - prompt: 'To be deleted', - name: 'Delete Test', - groupId: testPromptGroup._id, - type: 'text', - }, - }); - - // Check if savePrompt returned an error - if (!prompt || !prompt.prompt) { - throw new Error(`Failed to save prompt: ${prompt?.message || 'Unknown error'}`); - } - - const testPromptId = prompt.prompt._id; - const promptGroupId = testPromptGroup._id; - - // Grant permission - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: testUsers.owner._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: testUsers.owner._id, - }); - - // Verify ACL entry exists - const beforeDelete = await AclEntry.find({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - }); - expect(beforeDelete).toHaveLength(1); - - // Delete the prompt - await promptFns.deletePrompt({ - promptId: testPromptId, - groupId: promptGroupId, - author: testUsers.owner._id, - role: SystemRoles.USER, - }); - - // Verify ACL entries are removed - const aclEntries = await AclEntry.find({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: testPromptGroup._id, - }); - - expect(aclEntries).toHaveLength(0); - }); - }); - - describe('Backwards Compatibility', () => { - it('should handle prompts without ACL entries gracefully', async () => { - // Create a prompt group first - const promptGroup = await PromptGroup.create({ - name: 'Legacy Test Group', - author: testUsers.owner._id, - authorName: testUsers.owner.name, - productionId: new ObjectId(), - }); - - // Create a prompt without ACL entries (legacy prompt) - const legacyPrompt = await Prompt.create({ - prompt: 'Legacy prompt without ACL', - name: 'Legacy', - author: testUsers.owner._id, - groupId: promptGroup._id, - type: 'text', - }); - - // The system should handle this gracefully - const prompt = await promptFns.getPrompt({ _id: legacyPrompt._id }); - expect(prompt).toBeTruthy(); - expect(prompt._id.toString()).toBe(legacyPrompt._id.toString()); - }); - }); - - describe('deleteUserPrompts', () => { - let deletingUser; - let otherUser; - let soleOwnedGroup; - let multiOwnedGroup; - let sharedGroup; - let soleOwnedPrompt; - let multiOwnedPrompt; - let sharedPrompt; - - beforeAll(async () => { - deletingUser = await User.create({ - name: 'Deleting User', - email: 'deleting@example.com', - role: SystemRoles.USER, - }); - otherUser = await User.create({ - name: 'Other User', - email: 'other@example.com', - role: SystemRoles.USER, - }); - - const soleProductionId = new ObjectId(); - soleOwnedGroup = await PromptGroup.create({ - name: 'Sole Owned Group', - author: deletingUser._id, - authorName: deletingUser.name, - productionId: soleProductionId, - }); - soleOwnedPrompt = await Prompt.create({ - prompt: 'Sole owned prompt', - author: deletingUser._id, - groupId: soleOwnedGroup._id, - type: 'text', - }); - await PromptGroup.updateOne( - { _id: soleOwnedGroup._id }, - { productionId: soleOwnedPrompt._id }, - ); - - const multiProductionId = new ObjectId(); - multiOwnedGroup = await PromptGroup.create({ - name: 'Multi Owned Group', - author: deletingUser._id, - authorName: deletingUser.name, - productionId: multiProductionId, - }); - multiOwnedPrompt = await Prompt.create({ - prompt: 'Multi owned prompt', - author: deletingUser._id, - groupId: multiOwnedGroup._id, - type: 'text', - }); - await PromptGroup.updateOne( - { _id: multiOwnedGroup._id }, - { productionId: multiOwnedPrompt._id }, - ); - - const sharedProductionId = new ObjectId(); - sharedGroup = await PromptGroup.create({ - name: 'Shared Group (other user owns)', - author: otherUser._id, - authorName: otherUser.name, - productionId: sharedProductionId, - }); - sharedPrompt = await Prompt.create({ - prompt: 'Shared prompt', - author: otherUser._id, - groupId: sharedGroup._id, - type: 'text', - }); - await PromptGroup.updateOne({ _id: sharedGroup._id }, { productionId: sharedPrompt._id }); - - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: deletingUser._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: soleOwnedGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: deletingUser._id, - }); - - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: deletingUser._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: multiOwnedGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: deletingUser._id, - }); - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: otherUser._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: multiOwnedGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: otherUser._id, - }); - - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: otherUser._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: sharedGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - grantedBy: otherUser._id, - }); - await permissionService.grantPermission({ - principalType: PrincipalType.USER, - principalId: deletingUser._id, - resourceType: ResourceType.PROMPTGROUP, - resourceId: sharedGroup._id, - accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, - grantedBy: otherUser._id, - }); - - const globalProject = await Project.findOne({ name: 'Global' }); - await Project.updateOne( - { _id: globalProject._id }, - { - $addToSet: { - promptGroupIds: { - $each: [soleOwnedGroup._id, multiOwnedGroup._id, sharedGroup._id], - }, - }, - }, - ); - - await promptFns.deleteUserPrompts(deletingUser._id.toString()); - }); - - test('should delete solely-owned prompt groups and their prompts', async () => { - expect(await PromptGroup.findById(soleOwnedGroup._id)).toBeNull(); - expect(await Prompt.findById(soleOwnedPrompt._id)).toBeNull(); - }); - - test('should remove solely-owned groups from projects', async () => { - const globalProject = await Project.findOne({ name: 'Global' }); - const projectGroupIds = globalProject.promptGroupIds.map((id) => id.toString()); - expect(projectGroupIds).not.toContain(soleOwnedGroup._id.toString()); - }); - - test('should remove all ACL entries for solely-owned groups', async () => { - const aclEntries = await AclEntry.find({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: soleOwnedGroup._id, - }); - expect(aclEntries).toHaveLength(0); - }); - - test('should preserve multi-owned prompt groups', async () => { - expect(await PromptGroup.findById(multiOwnedGroup._id)).not.toBeNull(); - expect(await Prompt.findById(multiOwnedPrompt._id)).not.toBeNull(); - }); - - test('should preserve ACL entries of other owners on multi-owned groups', async () => { - const otherOwnerAcl = await AclEntry.findOne({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: multiOwnedGroup._id, - principalId: otherUser._id, - }); - expect(otherOwnerAcl).not.toBeNull(); - expect(otherOwnerAcl.permBits & PermissionBits.DELETE).toBeTruthy(); - }); - - test('should preserve groups owned by other users', async () => { - expect(await PromptGroup.findById(sharedGroup._id)).not.toBeNull(); - expect(await Prompt.findById(sharedPrompt._id)).not.toBeNull(); - }); - - test('should preserve project membership of non-deleted groups', async () => { - const globalProject = await Project.findOne({ name: 'Global' }); - const projectGroupIds = globalProject.promptGroupIds.map((id) => id.toString()); - expect(projectGroupIds).toContain(multiOwnedGroup._id.toString()); - expect(projectGroupIds).toContain(sharedGroup._id.toString()); - }); - - test('should preserve ACL entries for shared group owned by other user', async () => { - const ownerAcl = await AclEntry.findOne({ - resourceType: ResourceType.PROMPTGROUP, - resourceId: sharedGroup._id, - principalId: otherUser._id, - }); - expect(ownerAcl).not.toBeNull(); - }); - - test('should be a no-op when user has no owned prompt groups', async () => { - const unrelatedUser = await User.create({ - name: 'Unrelated User', - email: 'unrelated@example.com', - role: SystemRoles.USER, - }); - - const beforeCount = await PromptGroup.countDocuments(); - await promptFns.deleteUserPrompts(unrelatedUser._id.toString()); - const afterCount = await PromptGroup.countDocuments(); - - expect(afterCount).toBe(beforeCount); - }); - - test('should delete legacy prompt groups that have author but no ACL entries', async () => { - const legacyUser = await User.create({ - name: 'Legacy User', - email: 'legacy-prompt@example.com', - role: SystemRoles.USER, - }); - - const legacyGroup = await PromptGroup.create({ - name: 'Legacy Group (no ACL)', - author: legacyUser._id, - authorName: legacyUser.name, - productionId: new ObjectId(), - }); - const legacyPrompt = await Prompt.create({ - prompt: 'Legacy prompt text', - author: legacyUser._id, - groupId: legacyGroup._id, - type: 'text', - }); - - await promptFns.deleteUserPrompts(legacyUser._id.toString()); - - expect(await PromptGroup.findById(legacyGroup._id)).toBeNull(); - expect(await Prompt.findById(legacyPrompt._id)).toBeNull(); - }); - }); -}); diff --git a/api/models/Role.js b/api/models/Role.js deleted file mode 100644 index b7f806f3b6..0000000000 --- a/api/models/Role.js +++ /dev/null @@ -1,304 +0,0 @@ -const { - CacheKeys, - SystemRoles, - roleDefaults, - permissionsSchema, - removeNullishValues, -} = require('librechat-data-provider'); -const { logger } = require('@librechat/data-schemas'); -const getLogStores = require('~/cache/getLogStores'); -const { Role } = require('~/db/models'); - -/** - * Retrieve a role by name and convert the found role document to a plain object. - * If the role with the given name doesn't exist and the name is a system defined role, - * create it and return the lean version. - * - * @param {string} roleName - The name of the role to find or create. - * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. - * @returns {Promise} Role document. - */ -const getRoleByName = async function (roleName, fieldsToSelect = null) { - const cache = getLogStores(CacheKeys.ROLES); - try { - const cachedRole = await cache.get(roleName); - if (cachedRole) { - return cachedRole; - } - let query = Role.findOne({ name: roleName }); - if (fieldsToSelect) { - query = query.select(fieldsToSelect); - } - let role = await query.lean().exec(); - - if (!role && SystemRoles[roleName]) { - role = await new Role(roleDefaults[roleName]).save(); - await cache.set(roleName, role); - return role.toObject(); - } - await cache.set(roleName, role); - return role; - } catch (error) { - throw new Error(`Failed to retrieve or create role: ${error.message}`); - } -}; - -/** - * Update role values by name. - * - * @param {string} roleName - The name of the role to update. - * @param {Partial} updates - The fields to update. - * @returns {Promise} Updated role document. - */ -const updateRoleByName = async function (roleName, updates) { - const cache = getLogStores(CacheKeys.ROLES); - try { - const role = await Role.findOneAndUpdate( - { name: roleName }, - { $set: updates }, - { new: true, lean: true }, - ) - .select('-__v') - .lean() - .exec(); - await cache.set(roleName, role); - return role; - } catch (error) { - throw new Error(`Failed to update role: ${error.message}`); - } -}; - -/** - * Updates access permissions for a specific role and multiple permission types. - * @param {string} roleName - The role to update. - * @param {Object.>} permissionsUpdate - Permissions to update and their values. - * @param {IRole} [roleData] - Optional role data to use instead of fetching from the database. - */ -async function updateAccessPermissions(roleName, permissionsUpdate, roleData) { - // Filter and clean the permission updates based on our schema definition. - const updates = {}; - for (const [permissionType, permissions] of Object.entries(permissionsUpdate)) { - if (permissionsSchema.shape && permissionsSchema.shape[permissionType]) { - updates[permissionType] = removeNullishValues(permissions); - } - } - if (!Object.keys(updates).length) { - return; - } - - try { - const role = roleData ?? (await getRoleByName(roleName)); - if (!role) { - return; - } - - const currentPermissions = role.permissions || {}; - const updatedPermissions = { ...currentPermissions }; - let hasChanges = false; - - const unsetFields = {}; - const permissionTypes = Object.keys(permissionsSchema.shape || {}); - for (const permType of permissionTypes) { - if (role[permType] && typeof role[permType] === 'object') { - logger.info( - `Migrating '${roleName}' role from old schema: found '${permType}' at top level`, - ); - - updatedPermissions[permType] = { - ...updatedPermissions[permType], - ...role[permType], - }; - - unsetFields[permType] = 1; - hasChanges = true; - } - } - - // Migrate legacy SHARED_GLOBAL → SHARE for PROMPTS and AGENTS. - // SHARED_GLOBAL was removed in favour of SHARE in PR #11283. If the DB still has - // SHARED_GLOBAL but not SHARE, inherit the value so sharing intent is preserved. - const legacySharedGlobalTypes = ['PROMPTS', 'AGENTS']; - for (const legacyPermType of legacySharedGlobalTypes) { - const existingTypePerms = currentPermissions[legacyPermType]; - if ( - existingTypePerms && - 'SHARED_GLOBAL' in existingTypePerms && - !('SHARE' in existingTypePerms) && - updates[legacyPermType] && - // Don't override an explicit SHARE value the caller already provided - !('SHARE' in updates[legacyPermType]) - ) { - const inheritedValue = existingTypePerms['SHARED_GLOBAL']; - updates[legacyPermType]['SHARE'] = inheritedValue; - logger.info( - `Migrating '${roleName}' role ${legacyPermType}.SHARED_GLOBAL=${inheritedValue} → SHARE`, - ); - } - } - - for (const [permissionType, permissions] of Object.entries(updates)) { - const currentTypePermissions = currentPermissions[permissionType] || {}; - updatedPermissions[permissionType] = { ...currentTypePermissions }; - - for (const [permission, value] of Object.entries(permissions)) { - if (currentTypePermissions[permission] !== value) { - updatedPermissions[permissionType][permission] = value; - hasChanges = true; - logger.info( - `Updating '${roleName}' role permission '${permissionType}' '${permission}' from ${currentTypePermissions[permission]} to: ${value}`, - ); - } - } - } - - // Clean up orphaned SHARED_GLOBAL fields left in DB after the schema rename. - // Since we $set the full permissions object, deleting from updatedPermissions - // is sufficient to remove the field from MongoDB. - for (const legacyPermType of legacySharedGlobalTypes) { - const existingTypePerms = currentPermissions[legacyPermType]; - if (existingTypePerms && 'SHARED_GLOBAL' in existingTypePerms) { - if (!updates[legacyPermType]) { - // permType wasn't in the update payload so the migration block above didn't run. - // Create a writable copy and handle the SHARED_GLOBAL → SHARE inheritance here - // to avoid removing SHARED_GLOBAL without writing SHARE (data loss). - updatedPermissions[legacyPermType] = { ...existingTypePerms }; - if (!('SHARE' in existingTypePerms)) { - updatedPermissions[legacyPermType]['SHARE'] = existingTypePerms['SHARED_GLOBAL']; - logger.info( - `Migrating '${roleName}' role ${legacyPermType}.SHARED_GLOBAL=${existingTypePerms['SHARED_GLOBAL']} → SHARE`, - ); - } - } - delete updatedPermissions[legacyPermType]['SHARED_GLOBAL']; - hasChanges = true; - logger.info( - `Removed legacy SHARED_GLOBAL field from '${roleName}' role ${legacyPermType} permissions`, - ); - } - } - - if (hasChanges) { - const updateObj = { permissions: updatedPermissions }; - - if (Object.keys(unsetFields).length > 0) { - logger.info( - `Unsetting old schema fields for '${roleName}' role: ${Object.keys(unsetFields).join(', ')}`, - ); - - try { - await Role.updateOne( - { name: roleName }, - { - $set: updateObj, - $unset: unsetFields, - }, - ); - - const cache = getLogStores(CacheKeys.ROLES); - const updatedRole = await Role.findOne({ name: roleName }).select('-__v').lean().exec(); - await cache.set(roleName, updatedRole); - - logger.info(`Updated role '${roleName}' and removed old schema fields`); - } catch (updateError) { - logger.error(`Error during role migration update: ${updateError.message}`); - throw updateError; - } - } else { - // Standard update if no migration needed - await updateRoleByName(roleName, updateObj); - } - - logger.info(`Updated '${roleName}' role permissions`); - } else { - logger.info(`No changes needed for '${roleName}' role permissions`); - } - } catch (error) { - logger.error(`Failed to update ${roleName} role permissions:`, error); - } -} - -/** - * Migrates roles from old schema to new schema structure. - * This can be called directly to fix existing roles. - * - * @param {string} [roleName] - Optional specific role to migrate. If not provided, migrates all roles. - * @returns {Promise} Number of roles migrated. - */ -const migrateRoleSchema = async function (roleName) { - try { - // Get roles to migrate - let roles; - if (roleName) { - const role = await Role.findOne({ name: roleName }); - roles = role ? [role] : []; - } else { - roles = await Role.find({}); - } - - logger.info(`Migrating ${roles.length} roles to new schema structure`); - let migratedCount = 0; - - for (const role of roles) { - const permissionTypes = Object.keys(permissionsSchema.shape || {}); - const unsetFields = {}; - let hasOldSchema = false; - - // Check for old schema fields - for (const permType of permissionTypes) { - if (role[permType] && typeof role[permType] === 'object') { - hasOldSchema = true; - - // Ensure permissions object exists - role.permissions = role.permissions || {}; - - // Migrate permissions from old location to new - role.permissions[permType] = { - ...role.permissions[permType], - ...role[permType], - }; - - // Mark field for removal - unsetFields[permType] = 1; - } - } - - if (hasOldSchema) { - try { - logger.info(`Migrating role '${role.name}' from old schema structure`); - - // Simple update operation - await Role.updateOne( - { _id: role._id }, - { - $set: { permissions: role.permissions }, - $unset: unsetFields, - }, - ); - - // Refresh cache - const cache = getLogStores(CacheKeys.ROLES); - const updatedRole = await Role.findById(role._id).lean().exec(); - await cache.set(role.name, updatedRole); - - migratedCount++; - logger.info(`Migrated role '${role.name}'`); - } catch (error) { - logger.error(`Failed to migrate role '${role.name}': ${error.message}`); - } - } - } - - logger.info(`Migration complete: ${migratedCount} roles migrated`); - return migratedCount; - } catch (error) { - logger.error(`Role schema migration failed: ${error.message}`); - throw error; - } -}; - -module.exports = { - getRoleByName, - updateRoleByName, - migrateRoleSchema, - updateAccessPermissions, -}; diff --git a/api/models/ToolCall.js b/api/models/ToolCall.js deleted file mode 100644 index 689386114b..0000000000 --- a/api/models/ToolCall.js +++ /dev/null @@ -1,96 +0,0 @@ -const { ToolCall } = require('~/db/models'); - -/** - * Create a new tool call - * @param {IToolCallData} toolCallData - The tool call data - * @returns {Promise} The created tool call document - */ -async function createToolCall(toolCallData) { - try { - return await ToolCall.create(toolCallData); - } catch (error) { - throw new Error(`Error creating tool call: ${error.message}`); - } -} - -/** - * Get a tool call by ID - * @param {string} id - The tool call document ID - * @returns {Promise} The tool call document or null if not found - */ -async function getToolCallById(id) { - try { - return await ToolCall.findById(id).lean(); - } catch (error) { - throw new Error(`Error fetching tool call: ${error.message}`); - } -} - -/** - * Get tool calls by message ID and user - * @param {string} messageId - The message ID - * @param {string} userId - The user's ObjectId - * @returns {Promise} Array of tool call documents - */ -async function getToolCallsByMessage(messageId, userId) { - try { - return await ToolCall.find({ messageId, user: userId }).lean(); - } catch (error) { - throw new Error(`Error fetching tool calls: ${error.message}`); - } -} - -/** - * Get tool calls by conversation ID and user - * @param {string} conversationId - The conversation ID - * @param {string} userId - The user's ObjectId - * @returns {Promise} Array of tool call documents - */ -async function getToolCallsByConvo(conversationId, userId) { - try { - return await ToolCall.find({ conversationId, user: userId }).lean(); - } catch (error) { - throw new Error(`Error fetching tool calls: ${error.message}`); - } -} - -/** - * Update a tool call - * @param {string} id - The tool call document ID - * @param {Partial} updateData - The data to update - * @returns {Promise} The updated tool call document or null if not found - */ -async function updateToolCall(id, updateData) { - try { - return await ToolCall.findByIdAndUpdate(id, updateData, { new: true }).lean(); - } catch (error) { - throw new Error(`Error updating tool call: ${error.message}`); - } -} - -/** - * Delete a tool call - * @param {string} userId - The related user's ObjectId - * @param {string} [conversationId] - The tool call conversation ID - * @returns {Promise<{ ok?: number; n?: number; deletedCount?: number }>} The result of the delete operation - */ -async function deleteToolCalls(userId, conversationId) { - try { - const query = { user: userId }; - if (conversationId) { - query.conversationId = conversationId; - } - return await ToolCall.deleteMany(query); - } catch (error) { - throw new Error(`Error deleting tool call: ${error.message}`); - } -} - -module.exports = { - createToolCall, - updateToolCall, - deleteToolCalls, - getToolCallById, - getToolCallsByConvo, - getToolCallsByMessage, -}; diff --git a/api/models/Transaction.js b/api/models/Transaction.js deleted file mode 100644 index 7f018e1c30..0000000000 --- a/api/models/Transaction.js +++ /dev/null @@ -1,223 +0,0 @@ -const { logger, CANCEL_RATE } = require('@librechat/data-schemas'); -const { getMultiplier, getCacheMultiplier } = require('./tx'); -const { Transaction } = require('~/db/models'); -const { updateBalance } = require('~/models'); - -/** Method to calculate and set the tokenValue for a transaction */ -function calculateTokenValue(txn) { - const { valueKey, tokenType, model, endpointTokenConfig, inputTokenCount } = txn; - const multiplier = Math.abs( - getMultiplier({ valueKey, tokenType, model, endpointTokenConfig, inputTokenCount }), - ); - txn.rate = multiplier; - txn.tokenValue = txn.rawAmount * multiplier; - if (txn.context && txn.tokenType === 'completion' && txn.context === 'incomplete') { - txn.tokenValue = Math.ceil(txn.tokenValue * CANCEL_RATE); - txn.rate *= CANCEL_RATE; - } -} - -/** - * New static method to create an auto-refill transaction that does NOT trigger a balance update. - * @param {object} txData - Transaction data. - * @param {string} txData.user - The user ID. - * @param {string} txData.tokenType - The type of token. - * @param {string} txData.context - The context of the transaction. - * @param {number} txData.rawAmount - The raw amount of tokens. - * @returns {Promise} - The created transaction. - */ -async function createAutoRefillTransaction(txData) { - if (txData.rawAmount != null && isNaN(txData.rawAmount)) { - return; - } - const transaction = new Transaction(txData); - transaction.endpointTokenConfig = txData.endpointTokenConfig; - transaction.inputTokenCount = txData.inputTokenCount; - calculateTokenValue(transaction); - await transaction.save(); - - const balanceResponse = await updateBalance({ - user: transaction.user, - incrementValue: txData.rawAmount, - setValues: { lastRefill: new Date() }, - }); - const result = { - rate: transaction.rate, - user: transaction.user.toString(), - balance: balanceResponse.tokenCredits, - }; - logger.debug('[Balance.check] Auto-refill performed', result); - result.transaction = transaction; - return result; -} - -/** - * Static method to create a transaction and update the balance - * @param {txData} _txData - Transaction data. - */ -async function createTransaction(_txData) { - const { balance, transactions, ...txData } = _txData; - if (txData.rawAmount != null && isNaN(txData.rawAmount)) { - return; - } - - if (transactions?.enabled === false) { - return; - } - - const transaction = new Transaction(txData); - transaction.endpointTokenConfig = txData.endpointTokenConfig; - transaction.inputTokenCount = txData.inputTokenCount; - calculateTokenValue(transaction); - - await transaction.save(); - if (!balance?.enabled) { - return; - } - - let incrementValue = transaction.tokenValue; - const balanceResponse = await updateBalance({ - user: transaction.user, - incrementValue, - }); - - return { - rate: transaction.rate, - user: transaction.user.toString(), - balance: balanceResponse.tokenCredits, - [transaction.tokenType]: incrementValue, - }; -} - -/** - * Static method to create a structured transaction and update the balance - * @param {txData} _txData - Transaction data. - */ -async function createStructuredTransaction(_txData) { - const { balance, transactions, ...txData } = _txData; - if (transactions?.enabled === false) { - return; - } - - const transaction = new Transaction(txData); - transaction.endpointTokenConfig = txData.endpointTokenConfig; - transaction.inputTokenCount = txData.inputTokenCount; - - calculateStructuredTokenValue(transaction); - - await transaction.save(); - - if (!balance?.enabled) { - return; - } - - let incrementValue = transaction.tokenValue; - - const balanceResponse = await updateBalance({ - user: transaction.user, - incrementValue, - }); - - return { - rate: transaction.rate, - user: transaction.user.toString(), - balance: balanceResponse.tokenCredits, - [transaction.tokenType]: incrementValue, - }; -} - -/** Method to calculate token value for structured tokens */ -function calculateStructuredTokenValue(txn) { - if (!txn.tokenType) { - txn.tokenValue = txn.rawAmount; - return; - } - - const { model, endpointTokenConfig, inputTokenCount } = txn; - - if (txn.tokenType === 'prompt') { - const inputMultiplier = getMultiplier({ - tokenType: 'prompt', - model, - endpointTokenConfig, - inputTokenCount, - }); - const writeMultiplier = - getCacheMultiplier({ cacheType: 'write', model, endpointTokenConfig }) ?? inputMultiplier; - const readMultiplier = - getCacheMultiplier({ cacheType: 'read', model, endpointTokenConfig }) ?? inputMultiplier; - - txn.rateDetail = { - input: inputMultiplier, - write: writeMultiplier, - read: readMultiplier, - }; - - const totalPromptTokens = - Math.abs(txn.inputTokens || 0) + - Math.abs(txn.writeTokens || 0) + - Math.abs(txn.readTokens || 0); - - if (totalPromptTokens > 0) { - txn.rate = - (Math.abs(inputMultiplier * (txn.inputTokens || 0)) + - Math.abs(writeMultiplier * (txn.writeTokens || 0)) + - Math.abs(readMultiplier * (txn.readTokens || 0))) / - totalPromptTokens; - } else { - txn.rate = Math.abs(inputMultiplier); // Default to input rate if no tokens - } - - txn.tokenValue = -( - Math.abs(txn.inputTokens || 0) * inputMultiplier + - Math.abs(txn.writeTokens || 0) * writeMultiplier + - Math.abs(txn.readTokens || 0) * readMultiplier - ); - - txn.rawAmount = -totalPromptTokens; - } else if (txn.tokenType === 'completion') { - const multiplier = getMultiplier({ - tokenType: txn.tokenType, - model, - endpointTokenConfig, - inputTokenCount, - }); - txn.rate = Math.abs(multiplier); - txn.tokenValue = -Math.abs(txn.rawAmount) * multiplier; - txn.rawAmount = -Math.abs(txn.rawAmount); - } - - if (txn.context && txn.tokenType === 'completion' && txn.context === 'incomplete') { - txn.tokenValue = Math.ceil(txn.tokenValue * CANCEL_RATE); - txn.rate *= CANCEL_RATE; - if (txn.rateDetail) { - txn.rateDetail = Object.fromEntries( - Object.entries(txn.rateDetail).map(([k, v]) => [k, v * CANCEL_RATE]), - ); - } - } -} - -/** - * Queries and retrieves transactions based on a given filter. - * @async - * @function getTransactions - * @param {Object} filter - MongoDB filter object to apply when querying transactions. - * @returns {Promise} A promise that resolves to an array of matched transactions. - * @throws {Error} Throws an error if querying the database fails. - */ -async function getTransactions(filter) { - try { - return await Transaction.find(filter).lean(); - } catch (error) { - logger.error('Error querying transactions:', error); - throw error; - } -} - -module.exports = { - getTransactions, - createTransaction, - createAutoRefillTransaction, - createStructuredTransaction, -}; diff --git a/api/models/balanceMethods.js b/api/models/balanceMethods.js deleted file mode 100644 index e614872eac..0000000000 --- a/api/models/balanceMethods.js +++ /dev/null @@ -1,156 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { ViolationTypes } = require('librechat-data-provider'); -const { createAutoRefillTransaction } = require('./Transaction'); -const { logViolation } = require('~/cache'); -const { getMultiplier } = require('./tx'); -const { Balance } = require('~/db/models'); - -function isInvalidDate(date) { - return isNaN(date); -} - -/** - * Simple check method that calculates token cost and returns balance info. - * The auto-refill logic has been moved to balanceMethods.js to prevent circular dependencies. - */ -const checkBalanceRecord = async function ({ - user, - model, - endpoint, - valueKey, - tokenType, - amount, - endpointTokenConfig, -}) { - const multiplier = getMultiplier({ valueKey, tokenType, model, endpoint, endpointTokenConfig }); - const tokenCost = amount * multiplier; - - // Retrieve the balance record - let record = await Balance.findOne({ user }).lean(); - if (!record) { - logger.debug('[Balance.check] No balance record found for user', { user }); - return { - canSpend: false, - balance: 0, - tokenCost, - }; - } - let balance = record.tokenCredits; - - logger.debug('[Balance.check] Initial state', { - user, - model, - endpoint, - valueKey, - tokenType, - amount, - balance, - multiplier, - endpointTokenConfig: !!endpointTokenConfig, - }); - - // Only perform auto-refill if spending would bring the balance to 0 or below - if (balance - tokenCost <= 0 && record.autoRefillEnabled && record.refillAmount > 0) { - const lastRefillDate = new Date(record.lastRefill); - const now = new Date(); - if ( - isInvalidDate(lastRefillDate) || - now >= - addIntervalToDate(lastRefillDate, record.refillIntervalValue, record.refillIntervalUnit) - ) { - try { - /** @type {{ rate: number, user: string, balance: number, transaction: import('@librechat/data-schemas').ITransaction}} */ - const result = await createAutoRefillTransaction({ - user: user, - tokenType: 'credits', - context: 'autoRefill', - rawAmount: record.refillAmount, - }); - balance = result.balance; - } catch (error) { - logger.error('[Balance.check] Failed to record transaction for auto-refill', error); - } - } - } - - logger.debug('[Balance.check] Token cost', { tokenCost }); - return { canSpend: balance >= tokenCost, balance, tokenCost }; -}; - -/** - * Adds a time interval to a given date. - * @param {Date} date - The starting date. - * @param {number} value - The numeric value of the interval. - * @param {'seconds'|'minutes'|'hours'|'days'|'weeks'|'months'} unit - The unit of time. - * @returns {Date} A new Date representing the starting date plus the interval. - */ -const addIntervalToDate = (date, value, unit) => { - const result = new Date(date); - switch (unit) { - case 'seconds': - result.setSeconds(result.getSeconds() + value); - break; - case 'minutes': - result.setMinutes(result.getMinutes() + value); - break; - case 'hours': - result.setHours(result.getHours() + value); - break; - case 'days': - result.setDate(result.getDate() + value); - break; - case 'weeks': - result.setDate(result.getDate() + value * 7); - break; - case 'months': - result.setMonth(result.getMonth() + value); - break; - default: - break; - } - return result; -}; - -/** - * Checks the balance for a user and determines if they can spend a certain amount. - * If the user cannot spend the amount, it logs a violation and denies the request. - * - * @async - * @function - * @param {Object} params - The function parameters. - * @param {ServerRequest} params.req - The Express request object. - * @param {Express.Response} params.res - The Express response object. - * @param {Object} params.txData - The transaction data. - * @param {string} params.txData.user - The user ID or identifier. - * @param {('prompt' | 'completion')} params.txData.tokenType - The type of token. - * @param {number} params.txData.amount - The amount of tokens. - * @param {string} params.txData.model - The model name or identifier. - * @param {string} [params.txData.endpointTokenConfig] - The token configuration for the endpoint. - * @returns {Promise} Throws error if the user cannot spend the amount. - * @throws {Error} Throws an error if there's an issue with the balance check. - */ -const checkBalance = async ({ req, res, txData }) => { - const { canSpend, balance, tokenCost } = await checkBalanceRecord(txData); - if (canSpend) { - return true; - } - - const type = ViolationTypes.TOKEN_BALANCE; - const errorMessage = { - type, - balance, - tokenCost, - promptTokens: txData.amount, - }; - - if (txData.generations && txData.generations.length > 0) { - errorMessage.generations = txData.generations; - } - - await logViolation(req, res, type, errorMessage, 0); - throw new Error(JSON.stringify(errorMessage)); -}; - -module.exports = { - checkBalance, -}; diff --git a/api/models/index.js b/api/models/index.js index d0b10be079..03d5d3ec71 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,19 +1,13 @@ const mongoose = require('mongoose'); const { createMethods } = require('@librechat/data-schemas'); -const methods = createMethods(mongoose); -const { comparePassword } = require('./userMethods'); -const { - getMessage, - getMessages, - saveMessage, - recordMessage, - updateMessage, - deleteMessagesSince, - deleteMessages, -} = require('./Message'); -const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation'); -const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset'); -const { File } = require('~/db/models'); +const { matchModelName, findMatchingPattern } = require('@librechat/api'); +const getLogStores = require('~/cache/getLogStores'); + +const methods = createMethods(mongoose, { + matchModelName, + findMatchingPattern, + getCache: getLogStores, +}); const seedDatabase = async () => { await methods.initializeRoles(); @@ -24,25 +18,4 @@ const seedDatabase = async () => { module.exports = { ...methods, seedDatabase, - comparePassword, - - getMessage, - getMessages, - saveMessage, - recordMessage, - updateMessage, - deleteMessagesSince, - deleteMessages, - - getConvoTitle, - getConvo, - saveConvo, - deleteConvos, - - getPreset, - getPresets, - savePreset, - deletePresets, - - Files: File, }; diff --git a/api/models/interface.js b/api/models/interface.js deleted file mode 100644 index a79a8e747f..0000000000 --- a/api/models/interface.js +++ /dev/null @@ -1,24 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { updateInterfacePermissions: updateInterfacePerms } = require('@librechat/api'); -const { getRoleByName, updateAccessPermissions } = require('./Role'); - -/** - * Update interface permissions based on app configuration. - * Must be done independently from loading the app config. - * @param {AppConfig} appConfig - */ -async function updateInterfacePermissions(appConfig) { - try { - await updateInterfacePerms({ - appConfig, - getRoleByName, - updateAccessPermissions, - }); - } catch (error) { - logger.error('Error updating interface permissions:', error); - } -} - -module.exports = { - updateInterfacePermissions, -}; diff --git a/api/models/inviteUser.js b/api/models/inviteUser.js deleted file mode 100644 index eda8394225..0000000000 --- a/api/models/inviteUser.js +++ /dev/null @@ -1,68 +0,0 @@ -const mongoose = require('mongoose'); -const { logger, hashToken, getRandomValues } = require('@librechat/data-schemas'); -const { createToken, findToken } = require('~/models'); - -/** - * @module inviteUser - * @description This module provides functions to create and get user invites - */ - -/** - * @function createInvite - * @description This function creates a new user invite - * @param {string} email - The email of the user to invite - * @returns {Promise} A promise that resolves to the saved invite document - * @throws {Error} If there is an error creating the invite - */ -const createInvite = async (email) => { - try { - const token = await getRandomValues(32); - const hash = await hashToken(token); - const encodedToken = encodeURIComponent(token); - - const fakeUserId = new mongoose.Types.ObjectId(); - - await createToken({ - userId: fakeUserId, - email, - token: hash, - createdAt: Date.now(), - expiresIn: 604800, - }); - - return encodedToken; - } catch (error) { - logger.error('[createInvite] Error creating invite', error); - return { message: 'Error creating invite' }; - } -}; - -/** - * @function getInvite - * @description This function retrieves a user invite - * @param {string} encodedToken - The token of the invite to retrieve - * @param {string} email - The email of the user to validate - * @returns {Promise} A promise that resolves to the retrieved invite document - * @throws {Error} If there is an error retrieving the invite, if the invite does not exist, or if the email does not match - */ -const getInvite = async (encodedToken, email) => { - try { - const token = decodeURIComponent(encodedToken); - const hash = await hashToken(token); - const invite = await findToken({ token: hash, email }); - - if (!invite) { - throw new Error('Invite not found or email does not match'); - } - - return invite; - } catch (error) { - logger.error('[getInvite] Error getting invite:', error); - return { error: true, message: error.message }; - } -}; - -module.exports = { - createInvite, - getInvite, -}; diff --git a/api/models/loadAddedAgent.js b/api/models/loadAddedAgent.js deleted file mode 100644 index 101ee96685..0000000000 --- a/api/models/loadAddedAgent.js +++ /dev/null @@ -1,218 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { getCustomEndpointConfig } = require('@librechat/api'); -const { - Tools, - Constants, - isAgentsEndpoint, - isEphemeralAgentId, - appendAgentIdSuffix, - encodeEphemeralAgentId, -} = require('librechat-data-provider'); -const { getMCPServerTools } = require('~/server/services/Config'); - -const { mcp_all, mcp_delimiter } = Constants; - -/** - * Constant for added conversation agent ID - */ -const ADDED_AGENT_ID = 'added_agent'; - -/** - * Get an agent document based on the provided ID. - * @param {Object} searchParameter - The search parameters to find the agent. - * @param {string} searchParameter.id - The ID of the agent. - * @returns {Promise} - */ -let getAgent; - -/** - * Set the getAgent function (dependency injection to avoid circular imports) - * @param {Function} fn - */ -const setGetAgent = (fn) => { - getAgent = fn; -}; - -/** - * Load an agent from an added conversation (TConversation). - * Used for multi-convo parallel agent execution. - * - * @param {Object} params - * @param {import('express').Request} params.req - * @param {import('librechat-data-provider').TConversation} params.conversation - The added conversation - * @param {import('librechat-data-provider').Agent} [params.primaryAgent] - The primary agent (used to duplicate tools when both are ephemeral) - * @returns {Promise} The agent config as a plain object, or null if invalid. - */ -const loadAddedAgent = async ({ req, conversation, primaryAgent }) => { - if (!conversation) { - return null; - } - - if (conversation.agent_id && !isEphemeralAgentId(conversation.agent_id)) { - let agent = req.resolvedAddedAgent; - if (!agent) { - if (!getAgent) { - throw new Error('getAgent not initialized - call setGetAgent first'); - } - agent = await getAgent({ id: conversation.agent_id }); - } - - if (!agent) { - logger.warn(`[loadAddedAgent] Agent ${conversation.agent_id} not found`); - return null; - } - - agent.version = agent.versions ? agent.versions.length : 0; - // Append suffix to distinguish from primary agent (matches ephemeral format) - // This is needed when both agents have the same ID or for consistent parallel content attribution - agent.id = appendAgentIdSuffix(agent.id, 1); - return agent; - } - - // Otherwise, create an ephemeral agent config from the conversation - const { model, endpoint, promptPrefix, spec, ...rest } = conversation; - - if (!endpoint || !model) { - logger.warn('[loadAddedAgent] Missing required endpoint or model for ephemeral agent'); - return null; - } - - // If both primary and added agents are ephemeral, duplicate tools from primary agent - const primaryIsEphemeral = primaryAgent && isEphemeralAgentId(primaryAgent.id); - if (primaryIsEphemeral && Array.isArray(primaryAgent.tools)) { - // Get endpoint config and model spec for display name fallbacks - const appConfig = req.config; - let endpointConfig = appConfig?.endpoints?.[endpoint]; - if (!isAgentsEndpoint(endpoint) && !endpointConfig) { - try { - endpointConfig = getCustomEndpointConfig({ endpoint, appConfig }); - } catch (err) { - logger.error('[loadAddedAgent] Error getting custom endpoint config', err); - } - } - - // Look up model spec for label fallback - const modelSpecs = appConfig?.modelSpecs?.list; - const modelSpec = spec != null && spec !== '' ? modelSpecs?.find((s) => s.name === spec) : null; - - // For ephemeral agents, use modelLabel if provided, then model spec's label, - // then modelDisplayLabel from endpoint config, otherwise empty string to show model name - const sender = rest.modelLabel ?? modelSpec?.label ?? endpointConfig?.modelDisplayLabel ?? ''; - - const ephemeralId = encodeEphemeralAgentId({ endpoint, model, sender, index: 1 }); - - return { - id: ephemeralId, - instructions: promptPrefix || '', - provider: endpoint, - model_parameters: {}, - model, - tools: [...primaryAgent.tools], - }; - } - - // Extract ephemeral agent options from conversation if present - const ephemeralAgent = rest.ephemeralAgent; - const mcpServers = new Set(ephemeralAgent?.mcp); - const userId = req.user?.id; - - // Check model spec for MCP servers - const modelSpecs = req.config?.modelSpecs?.list; - let modelSpec = null; - if (spec != null && spec !== '') { - modelSpec = modelSpecs?.find((s) => s.name === spec) || null; - } - if (modelSpec?.mcpServers) { - for (const mcpServer of modelSpec.mcpServers) { - mcpServers.add(mcpServer); - } - } - - /** @type {string[]} */ - const tools = []; - if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) { - tools.push(Tools.execute_code); - } - if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) { - tools.push(Tools.file_search); - } - if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) { - tools.push(Tools.web_search); - } - - const addedServers = new Set(); - if (mcpServers.size > 0) { - for (const mcpServer of mcpServers) { - if (addedServers.has(mcpServer)) { - continue; - } - const serverTools = await getMCPServerTools(userId, mcpServer); - if (!serverTools) { - tools.push(`${mcp_all}${mcp_delimiter}${mcpServer}`); - addedServers.add(mcpServer); - continue; - } - tools.push(...Object.keys(serverTools)); - addedServers.add(mcpServer); - } - } - - // Build model_parameters from conversation fields - const model_parameters = {}; - const paramKeys = [ - 'temperature', - 'top_p', - 'topP', - 'topK', - 'presence_penalty', - 'frequency_penalty', - 'maxOutputTokens', - 'maxTokens', - 'max_tokens', - ]; - - for (const key of paramKeys) { - if (rest[key] != null) { - model_parameters[key] = rest[key]; - } - } - - // Get endpoint config for modelDisplayLabel fallback - const appConfig = req.config; - let endpointConfig = appConfig?.endpoints?.[endpoint]; - if (!isAgentsEndpoint(endpoint) && !endpointConfig) { - try { - endpointConfig = getCustomEndpointConfig({ endpoint, appConfig }); - } catch (err) { - logger.error('[loadAddedAgent] Error getting custom endpoint config', err); - } - } - - // For ephemeral agents, use modelLabel if provided, then model spec's label, - // then modelDisplayLabel from endpoint config, otherwise empty string to show model name - const sender = rest.modelLabel ?? modelSpec?.label ?? endpointConfig?.modelDisplayLabel ?? ''; - - /** Encoded ephemeral agent ID with endpoint, model, sender, and index=1 to distinguish from primary */ - const ephemeralId = encodeEphemeralAgentId({ endpoint, model, sender, index: 1 }); - - const result = { - id: ephemeralId, - instructions: promptPrefix || '', - provider: endpoint, - model_parameters, - model, - tools, - }; - - if (ephemeralAgent?.artifacts != null && ephemeralAgent.artifacts) { - result.artifacts = ephemeralAgent.artifacts; - } - - return result; -}; - -module.exports = { - ADDED_AGENT_ID, - loadAddedAgent, - setGetAgent, -}; diff --git a/api/models/spendTokens.js b/api/models/spendTokens.js deleted file mode 100644 index afe05969d8..0000000000 --- a/api/models/spendTokens.js +++ /dev/null @@ -1,140 +0,0 @@ -const { logger } = require('@librechat/data-schemas'); -const { createTransaction, createStructuredTransaction } = require('./Transaction'); -/** - * Creates up to two transactions to record the spending of tokens. - * - * @function - * @async - * @param {txData} txData - Transaction data. - * @param {Object} tokenUsage - The number of tokens used. - * @param {Number} tokenUsage.promptTokens - The number of prompt tokens used. - * @param {Number} tokenUsage.completionTokens - The number of completion tokens used. - * @returns {Promise} - Returns nothing. - * @throws {Error} - Throws an error if there's an issue creating the transactions. - */ -const spendTokens = async (txData, tokenUsage) => { - const { promptTokens, completionTokens } = tokenUsage; - logger.debug( - `[spendTokens] conversationId: ${txData.conversationId}${ - txData?.context ? ` | Context: ${txData?.context}` : '' - } | Token usage: `, - { - promptTokens, - completionTokens, - }, - ); - let prompt, completion; - const normalizedPromptTokens = Math.max(promptTokens ?? 0, 0); - try { - if (promptTokens !== undefined) { - prompt = await createTransaction({ - ...txData, - tokenType: 'prompt', - rawAmount: promptTokens === 0 ? 0 : -normalizedPromptTokens, - inputTokenCount: normalizedPromptTokens, - }); - } - - if (completionTokens !== undefined) { - completion = await createTransaction({ - ...txData, - tokenType: 'completion', - rawAmount: completionTokens === 0 ? 0 : -Math.max(completionTokens, 0), - inputTokenCount: normalizedPromptTokens, - }); - } - - if (prompt || completion) { - logger.debug('[spendTokens] Transaction data record against balance:', { - user: txData.user, - prompt: prompt?.prompt, - promptRate: prompt?.rate, - completion: completion?.completion, - completionRate: completion?.rate, - balance: completion?.balance ?? prompt?.balance, - }); - } else { - logger.debug('[spendTokens] No transactions incurred against balance'); - } - } catch (err) { - logger.error('[spendTokens]', err); - } -}; - -/** - * Creates transactions to record the spending of structured tokens. - * - * @function - * @async - * @param {txData} txData - Transaction data. - * @param {Object} tokenUsage - The number of tokens used. - * @param {Object} tokenUsage.promptTokens - The number of prompt tokens used. - * @param {Number} tokenUsage.promptTokens.input - The number of input tokens. - * @param {Number} tokenUsage.promptTokens.write - The number of write tokens. - * @param {Number} tokenUsage.promptTokens.read - The number of read tokens. - * @param {Number} tokenUsage.completionTokens - The number of completion tokens used. - * @returns {Promise} - Returns nothing. - * @throws {Error} - Throws an error if there's an issue creating the transactions. - */ -const spendStructuredTokens = async (txData, tokenUsage) => { - const { promptTokens, completionTokens } = tokenUsage; - logger.debug( - `[spendStructuredTokens] conversationId: ${txData.conversationId}${ - txData?.context ? ` | Context: ${txData?.context}` : '' - } | Token usage: `, - { - promptTokens, - completionTokens, - }, - ); - let prompt, completion; - try { - if (promptTokens) { - const input = Math.max(promptTokens.input ?? 0, 0); - const write = Math.max(promptTokens.write ?? 0, 0); - const read = Math.max(promptTokens.read ?? 0, 0); - const totalInputTokens = input + write + read; - prompt = await createStructuredTransaction({ - ...txData, - tokenType: 'prompt', - inputTokens: -input, - writeTokens: -write, - readTokens: -read, - inputTokenCount: totalInputTokens, - }); - } - - if (completionTokens) { - const totalInputTokens = promptTokens - ? Math.max(promptTokens.input ?? 0, 0) + - Math.max(promptTokens.write ?? 0, 0) + - Math.max(promptTokens.read ?? 0, 0) - : undefined; - completion = await createTransaction({ - ...txData, - tokenType: 'completion', - rawAmount: -Math.max(completionTokens, 0), - inputTokenCount: totalInputTokens, - }); - } - - if (prompt || completion) { - logger.debug('[spendStructuredTokens] Transaction data record against balance:', { - user: txData.user, - prompt: prompt?.prompt, - promptRate: prompt?.rate, - completion: completion?.completion, - completionRate: completion?.rate, - balance: completion?.balance ?? prompt?.balance, - }); - } else { - logger.debug('[spendStructuredTokens] No transactions incurred against balance'); - } - } catch (err) { - logger.error('[spendStructuredTokens]', err); - } - - return { prompt, completion }; -}; - -module.exports = { spendTokens, spendStructuredTokens }; diff --git a/api/models/userMethods.js b/api/models/userMethods.js deleted file mode 100644 index b57b24e641..0000000000 --- a/api/models/userMethods.js +++ /dev/null @@ -1,31 +0,0 @@ -const bcrypt = require('bcryptjs'); - -/** - * Compares the provided password with the user's password. - * - * @param {IUser} user - The user to compare the password for. - * @param {string} candidatePassword - The password to test against the user's password. - * @returns {Promise} A promise that resolves to a boolean indicating if the password matches. - */ -const comparePassword = async (user, candidatePassword) => { - if (!user) { - throw new Error('No user provided'); - } - - if (!user.password) { - throw new Error('No password, likely an email first registered via Social/OIDC login'); - } - - return new Promise((resolve, reject) => { - bcrypt.compare(candidatePassword, user.password, (err, isMatch) => { - if (err) { - reject(err); - } - resolve(isMatch); - }); - }); -}; - -module.exports = { - comparePassword, -}; diff --git a/api/server/controllers/Balance.js b/api/server/controllers/Balance.js index c892a73b0c..fd9b32e74c 100644 --- a/api/server/controllers/Balance.js +++ b/api/server/controllers/Balance.js @@ -1,24 +1,22 @@ -const { Balance } = require('~/db/models'); +const { findBalanceByUser } = require('~/models'); async function balanceController(req, res) { - const balanceData = await Balance.findOne( - { user: req.user.id }, - '-_id tokenCredits autoRefillEnabled refillIntervalValue refillIntervalUnit lastRefill refillAmount', - ).lean(); + const balanceData = await findBalanceByUser(req.user.id); if (!balanceData) { return res.status(404).json({ error: 'Balance not found' }); } - // If auto-refill is not enabled, remove auto-refill related fields from the response - if (!balanceData.autoRefillEnabled) { - delete balanceData.refillIntervalValue; - delete balanceData.refillIntervalUnit; - delete balanceData.lastRefill; - delete balanceData.refillAmount; + const { _id: _, ...result } = balanceData; + + if (!result.autoRefillEnabled) { + delete result.refillIntervalValue; + delete result.refillIntervalUnit; + delete result.lastRefill; + delete result.refillAmount; } - res.status(200).json(balanceData); + res.status(200).json(result); } module.exports = balanceController; diff --git a/api/server/controllers/PermissionsController.js b/api/server/controllers/PermissionsController.js index 16930c5139..59732572c0 100644 --- a/api/server/controllers/PermissionsController.js +++ b/api/server/controllers/PermissionsController.js @@ -9,16 +9,19 @@ const { enrichRemoteAgentPrincipals, backfillRemoteAgentPermissions } = require( const { bulkUpdateResourcePermissions, ensureGroupPrincipalExists, + getResourcePermissionsMap, + findAccessibleResources, getEffectivePermissions, ensurePrincipalExists, getAvailableRoles, - findAccessibleResources, - getResourcePermissionsMap, } = require('~/server/services/PermissionService'); const { searchPrincipals: searchLocalPrincipals, sortPrincipalsByRelevance, calculateRelevanceScore, + findRoleByIdentifier, + aggregateAclEntries, + bulkWriteAclEntries, } = require('~/models'); const { entraIdPrincipalFeatureEnabled, @@ -217,8 +220,7 @@ const getResourcePermissions = async (req, res) => { const { resourceType, resourceId } = req.params; validateResourceType(resourceType); - // Use aggregation pipeline for efficient single-query data retrieval - const results = await AclEntry.aggregate([ + const results = await aggregateAclEntries([ // Match ACL entries for this resource { $match: { @@ -314,7 +316,12 @@ const getResourcePermissions = async (req, res) => { } if (resourceType === ResourceType.REMOTE_AGENT) { - const enricherDeps = { AclEntry, AccessRole, logger }; + const enricherDeps = { + aggregateAclEntries, + bulkWriteAclEntries, + findRoleByIdentifier, + logger, + }; const enrichResult = await enrichRemoteAgentPrincipals(enricherDeps, resourceId, principals); principals = enrichResult.principals; backfillRemoteAgentPermissions(enricherDeps, resourceId, enrichResult.entriesToBackfill); diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 48f34479cd..4a1c9135ab 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -13,34 +13,6 @@ const { FileSources, ResourceType, } = require('librechat-data-provider'); -const { - deleteAllUserSessions, - deleteAllSharedLinks, - updateUserPlugins, - deleteUserById, - deleteMessages, - deletePresets, - deleteUserKey, - getUserById, - deleteConvos, - deleteFiles, - updateUser, - findToken, - getFiles, -} = require('~/models'); -const { - ConversationTag, - AgentApiKey, - Transaction, - MemoryEntry, - Assistant, - AclEntry, - Balance, - Action, - Group, - Token, - User, -} = require('~/db/models'); const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); const { verifyOTPOrBackupCode } = require('~/server/services/twoFactorService'); const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService'); @@ -49,11 +21,9 @@ const { invalidateCachedTools } = require('~/server/services/Config/getCachedToo const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud'); const { processDeleteRequest } = require('~/server/services/Files/process'); const { getAppConfig } = require('~/server/services/Config'); -const { deleteToolCalls } = require('~/models/ToolCall'); -const { deleteUserPrompts } = require('~/models/Prompt'); -const { deleteUserAgents } = require('~/models/Agent'); const { getSoleOwnedResourceIds } = require('~/server/services/PermissionService'); const { getLogStores } = require('~/cache'); +const db = require('~/models'); const getUserController = async (req, res) => { const appConfig = await getAppConfig({ role: req.user?.role }); @@ -74,7 +44,7 @@ const getUserController = async (req, res) => { const originalAvatar = userData.avatar; try { userData.avatar = await getNewS3URL(userData.avatar); - await updateUser(userData.id, { avatar: userData.avatar }); + await db.updateUser(userData.id, { avatar: userData.avatar }); } catch (error) { userData.avatar = originalAvatar; logger.error('Error getting new S3 URL for avatar:', error); @@ -85,7 +55,7 @@ const getUserController = async (req, res) => { const getTermsStatusController = async (req, res) => { try { - const user = await User.findById(req.user.id); + const user = await db.getUserById(req.user.id, 'termsAccepted'); if (!user) { return res.status(404).json({ message: 'User not found' }); } @@ -98,7 +68,7 @@ const getTermsStatusController = async (req, res) => { const acceptTermsController = async (req, res) => { try { - const user = await User.findByIdAndUpdate(req.user.id, { termsAccepted: true }, { new: true }); + const user = await db.updateUser(req.user.id, { termsAccepted: true }); if (!user) { return res.status(404).json({ message: 'User not found' }); } @@ -111,7 +81,7 @@ const acceptTermsController = async (req, res) => { const deleteUserFiles = async (req) => { try { - const userFiles = await getFiles({ user: req.user.id }); + const userFiles = await db.getFiles({ user: req.user.id }); await processDeleteRequest({ req, files: userFiles, @@ -134,6 +104,7 @@ const deleteUserFiles = async (req) => { const deleteUserMcpServers = async (userId) => { try { const MCPServer = mongoose.models.MCPServer; + const AclEntry = mongoose.models.AclEntry; if (!MCPServer) { return; } @@ -199,7 +170,7 @@ const updateUserPluginsController = async (req, res) => { const { pluginKey, action, auth, isEntityTool } = req.body; try { if (!isEntityTool) { - await updateUserPlugins(user._id, user.plugins, pluginKey, action); + await db.updateUserPlugins(user._id, user.plugins, pluginKey, action); } if (auth == null) { @@ -323,7 +294,7 @@ const deleteUserController = async (req, res) => { const { user } = req; try { - const existingUser = await getUserById( + const existingUser = await db.getUserById( user.id, '+totpSecret +backupCodes _id twoFactorEnabled', ); @@ -339,34 +310,34 @@ const deleteUserController = async (req, res) => { } } - await deleteMessages({ user: user.id }); // delete user messages - await deleteAllUserSessions({ userId: user.id }); // delete user sessions - await Transaction.deleteMany({ user: user.id }); // delete user transactions - await deleteUserKey({ userId: user.id, all: true }); // delete user keys - await Balance.deleteMany({ user: user._id }); // delete user balances - await deletePresets(user.id); // delete user presets + await db.deleteMessages({ user: user.id }); + await db.deleteAllUserSessions({ userId: user.id }); + await db.deleteTransactions({ user: user.id }); + await db.deleteUserKey({ userId: user.id, all: true }); + await db.deleteBalances({ user: user._id }); + await db.deletePresets(user.id); try { - await deleteConvos(user.id); // delete user convos + await db.deleteConvos(user.id); } catch (error) { logger.error('[deleteUserController] Error deleting user convos, likely no convos', error); } - await deleteUserPluginAuth(user.id, null, true); // delete user plugin auth - await deleteUserById(user.id); // delete user - await deleteAllSharedLinks(user.id); // delete user shared links - await deleteUserFiles(req); // delete user files - await deleteFiles(null, user.id); // delete database files in case of orphaned files from previous steps - await deleteToolCalls(user.id); // delete user tool calls - await deleteUserAgents(user.id); // delete user agents - await AgentApiKey.deleteMany({ user: user._id }); // delete user agent API keys - await Assistant.deleteMany({ user: user.id }); // delete user assistants - await ConversationTag.deleteMany({ user: user.id }); // delete user conversation tags - await MemoryEntry.deleteMany({ userId: user.id }); // delete user memory entries - await deleteUserPrompts(user.id); // delete user prompts - await deleteUserMcpServers(user.id); // delete user MCP servers - await Action.deleteMany({ user: user.id }); // delete user actions - await Token.deleteMany({ userId: user.id }); // delete user OAuth tokens - await Group.updateMany({ memberIds: user.id }, { $pullAll: { memberIds: [user.id] } }); - await AclEntry.deleteMany({ principalId: user._id }); // delete user ACL entries + await deleteUserPluginAuth(user.id, null, true); + await db.deleteUserById(user.id); + await db.deleteAllSharedLinks(user.id); + await deleteUserFiles(req); + await db.deleteFiles(null, user.id); + await db.deleteToolCalls(user.id); + await db.deleteUserAgents(user.id); + await db.deleteAllAgentApiKeys(user._id); + await db.deleteAssistants({ user: user.id }); + await db.deleteConversationTags({ user: user.id }); + await db.deleteAllUserMemories(user.id); + await db.deleteUserPrompts(user.id); + await deleteUserMcpServers(user.id); + await db.deleteActions({ user: user.id }); + await db.deleteTokens({ userId: user.id }); + await db.removeUserFromAllGroups(user.id); + await db.deleteAclEntries({ principalId: user._id }); logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`); res.status(200).send({ message: 'User deleted' }); } catch (err) { @@ -426,7 +397,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { const clientTokenData = await MCPTokenStorage.getClientInfoAndMetadata({ userId, serverName, - findToken, + findToken: db.findToken, }); if (clientTokenData == null) { return; @@ -437,7 +408,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { const tokens = await MCPTokenStorage.getTokens({ userId, serverName, - findToken, + findToken: db.findToken, }); // 3. revoke OAuth tokens at the provider @@ -496,7 +467,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { userId, serverName, deleteToken: async (filter) => { - await Token.deleteOne(filter); + await db.deleteTokens(filter); }, }); diff --git a/api/server/controllers/UserController.spec.js b/api/server/controllers/UserController.spec.js index cf5d971e02..6c96f067b7 100644 --- a/api/server/controllers/UserController.spec.js +++ b/api/server/controllers/UserController.spec.js @@ -14,20 +14,40 @@ jest.mock('@librechat/data-schemas', () => { }; }); -jest.mock('~/models', () => ({ - deleteAllUserSessions: jest.fn().mockResolvedValue(undefined), - deleteAllSharedLinks: jest.fn().mockResolvedValue(undefined), - updateUserPlugins: jest.fn(), - deleteUserById: jest.fn().mockResolvedValue(undefined), - deleteMessages: jest.fn().mockResolvedValue(undefined), - deletePresets: jest.fn().mockResolvedValue(undefined), - deleteUserKey: jest.fn().mockResolvedValue(undefined), - deleteConvos: jest.fn().mockResolvedValue(undefined), - deleteFiles: jest.fn().mockResolvedValue(undefined), - updateUser: jest.fn(), - findToken: jest.fn(), - getFiles: jest.fn().mockResolvedValue([]), -})); +jest.mock('~/models', () => { + const _mongoose = require('mongoose'); + return { + deleteAllUserSessions: jest.fn().mockResolvedValue(undefined), + deleteAllSharedLinks: jest.fn().mockResolvedValue(undefined), + deleteAllAgentApiKeys: jest.fn().mockResolvedValue(undefined), + deleteConversationTags: jest.fn().mockResolvedValue(undefined), + deleteAllUserMemories: jest.fn().mockResolvedValue(undefined), + deleteTransactions: jest.fn().mockResolvedValue(undefined), + deleteAclEntries: jest.fn().mockResolvedValue(undefined), + updateUserPlugins: jest.fn(), + deleteAssistants: jest.fn().mockResolvedValue(undefined), + deleteUserById: jest.fn().mockResolvedValue(undefined), + deleteUserPrompts: jest.fn().mockResolvedValue(undefined), + deleteMessages: jest.fn().mockResolvedValue(undefined), + deleteBalances: jest.fn().mockResolvedValue(undefined), + deleteActions: jest.fn().mockResolvedValue(undefined), + deletePresets: jest.fn().mockResolvedValue(undefined), + deleteUserKey: jest.fn().mockResolvedValue(undefined), + deleteToolCalls: jest.fn().mockResolvedValue(undefined), + deleteUserAgents: jest.fn().mockResolvedValue(undefined), + deleteTokens: jest.fn().mockResolvedValue(undefined), + deleteConvos: jest.fn().mockResolvedValue(undefined), + deleteFiles: jest.fn().mockResolvedValue(undefined), + updateUser: jest.fn(), + getUserById: jest.fn().mockResolvedValue(null), + findToken: jest.fn(), + getFiles: jest.fn().mockResolvedValue([]), + removeUserFromAllGroups: jest.fn().mockImplementation(async (userId) => { + const Group = _mongoose.models.Group; + await Group.updateMany({ memberIds: userId }, { $pullAll: { memberIds: [userId] } }); + }), + }; +}); jest.mock('~/server/services/PluginService', () => ({ updateUserPluginAuth: jest.fn(), @@ -55,18 +75,6 @@ jest.mock('~/server/services/Config', () => ({ getMCPServersRegistry: jest.fn(), })); -jest.mock('~/models/ToolCall', () => ({ - deleteToolCalls: jest.fn().mockResolvedValue(undefined), -})); - -jest.mock('~/models/Prompt', () => ({ - deleteUserPrompts: jest.fn().mockResolvedValue(undefined), -})); - -jest.mock('~/models/Agent', () => ({ - deleteUserAgents: jest.fn().mockResolvedValue(undefined), -})); - jest.mock('~/cache', () => ({ getLogStores: jest.fn(), })); diff --git a/api/server/controllers/agents/__tests__/openai.spec.js b/api/server/controllers/agents/__tests__/openai.spec.js index 50c61b7288..cc43387560 100644 --- a/api/server/controllers/agents/__tests__/openai.spec.js +++ b/api/server/controllers/agents/__tests__/openai.spec.js @@ -77,11 +77,6 @@ jest.mock('~/server/services/ToolService', () => ({ loadToolsForExecution: jest.fn().mockResolvedValue([]), })); -jest.mock('~/models/spendTokens', () => ({ - spendTokens: mockSpendTokens, - spendStructuredTokens: mockSpendStructuredTokens, -})); - const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); jest.mock('~/models/tx', () => ({ @@ -89,6 +84,7 @@ jest.mock('~/models/tx', () => ({ getCacheMultiplier: mockGetCacheMultiplier, })); + jest.mock('~/server/controllers/agents/callbacks', () => ({ createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), })); @@ -97,23 +93,11 @@ jest.mock('~/server/services/PermissionService', () => ({ findAccessibleResources: jest.fn().mockResolvedValue([]), })); -jest.mock('~/models/Conversation', () => ({ - getConvoFiles: jest.fn().mockResolvedValue([]), - getConvo: jest.fn().mockResolvedValue(null), -})); - -jest.mock('~/models/Agent', () => ({ - getAgent: jest.fn().mockResolvedValue({ - id: 'agent-123', - provider: 'openAI', - model_parameters: { model: 'gpt-4' }, - }), - getAgents: jest.fn().mockResolvedValue([]), -})); - const mockUpdateBalance = jest.fn().mockResolvedValue({}); const mockBulkInsertTransactions = jest.fn().mockResolvedValue(undefined); + jest.mock('~/models', () => ({ + getAgent: jest.fn().mockResolvedValue({ id: 'agent-123', name: 'Test Agent' }), getFiles: jest.fn(), getUserKey: jest.fn(), getMessages: jest.fn(), @@ -124,6 +108,9 @@ jest.mock('~/models', () => ({ getCodeGeneratedFiles: jest.fn(), updateBalance: mockUpdateBalance, bulkInsertTransactions: mockBulkInsertTransactions, + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + getConvoFiles: jest.fn().mockResolvedValue([]), })); describe('OpenAIChatCompletionController', () => { diff --git a/api/server/controllers/agents/__tests__/responses.unit.spec.js b/api/server/controllers/agents/__tests__/responses.unit.spec.js index e34f0ccf73..604c28f74d 100644 --- a/api/server/controllers/agents/__tests__/responses.unit.spec.js +++ b/api/server/controllers/agents/__tests__/responses.unit.spec.js @@ -101,11 +101,6 @@ jest.mock('~/server/services/ToolService', () => ({ loadToolsForExecution: jest.fn().mockResolvedValue([]), })); -jest.mock('~/models/spendTokens', () => ({ - spendTokens: mockSpendTokens, - spendStructuredTokens: mockSpendStructuredTokens, -})); - const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); jest.mock('~/models/tx', () => ({ @@ -113,6 +108,7 @@ jest.mock('~/models/tx', () => ({ getCacheMultiplier: mockGetCacheMultiplier, })); + jest.mock('~/server/controllers/agents/callbacks', () => ({ createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), createResponsesToolEndCallback: jest.fn().mockReturnValue(jest.fn()), @@ -122,25 +118,11 @@ jest.mock('~/server/services/PermissionService', () => ({ findAccessibleResources: jest.fn().mockResolvedValue([]), })); -jest.mock('~/models/Conversation', () => ({ - getConvoFiles: jest.fn().mockResolvedValue([]), - saveConvo: jest.fn().mockResolvedValue({}), - getConvo: jest.fn().mockResolvedValue(null), -})); - -jest.mock('~/models/Agent', () => ({ - getAgent: jest.fn().mockResolvedValue({ - id: 'agent-123', - name: 'Test Agent', - provider: 'anthropic', - model_parameters: { model: 'claude-3' }, - }), - getAgents: jest.fn().mockResolvedValue([]), -})); - const mockUpdateBalance = jest.fn().mockResolvedValue({}); const mockBulkInsertTransactions = jest.fn().mockResolvedValue(undefined); + jest.mock('~/models', () => ({ + getAgent: jest.fn().mockResolvedValue({ id: 'agent-123', name: 'Test Agent' }), getFiles: jest.fn(), getUserKey: jest.fn(), getMessages: jest.fn().mockResolvedValue([]), @@ -152,6 +134,11 @@ jest.mock('~/models', () => ({ getCodeGeneratedFiles: jest.fn(), updateBalance: mockUpdateBalance, bulkInsertTransactions: mockBulkInsertTransactions, + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + getConvoFiles: jest.fn().mockResolvedValue([]), + saveConvo: jest.fn().mockResolvedValue({}), + getConvo: jest.fn().mockResolvedValue(null), })); describe('createResponse controller', () => { diff --git a/api/server/controllers/agents/__tests__/v1.spec.js b/api/server/controllers/agents/__tests__/v1.spec.js index b7e7b67a22..39cf994fef 100644 --- a/api/server/controllers/agents/__tests__/v1.spec.js +++ b/api/server/controllers/agents/__tests__/v1.spec.js @@ -1,10 +1,8 @@ const { duplicateAgent } = require('../v1'); -const { getAgent, createAgent } = require('~/models/Agent'); -const { getActions } = require('~/models/Action'); +const { getAgent, createAgent, getActions } = require('~/models'); const { nanoid } = require('nanoid'); -jest.mock('~/models/Agent'); -jest.mock('~/models/Action'); +jest.mock('~/models'); jest.mock('nanoid'); describe('duplicateAgent', () => { diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index c454bd65cf..1724e20ada 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -22,6 +22,7 @@ const { GenerationJobManager, getTransactionsConfig, createMemoryProcessor, + loadAgent: loadAgentFn, createMultiAgentMapper, filterMalformedContentParts, } = require('@librechat/api'); @@ -45,18 +46,17 @@ const { removeNullishValues, } = require('librechat-data-provider'); const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); -const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); const { encodeAndFormat } = require('~/server/services/Files/images/encode'); const { updateBalance, bulkInsertTransactions } = require('~/models'); const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); const { createContextHandlers } = require('~/app/clients/prompts'); -const { getConvoFiles } = require('~/models/Conversation'); +const { getMCPServerTools } = require('~/server/services/Config'); const BaseClient = require('~/app/clients/BaseClient'); -const { getRoleByName } = require('~/models/Role'); -const { loadAgent } = require('~/models/Agent'); const { getMCPManager } = require('~/config'); const db = require('~/models'); +const loadAgent = (params) => loadAgentFn(params, { getAgent: db.getAgent, getMCPServerTools }); + class AgentClient extends BaseClient { constructor(options = {}) { super(null, options); @@ -413,7 +413,7 @@ class AgentClient extends BaseClient { user, permissionType: PermissionTypes.MEMORIES, permissions: [Permissions.USE], - getRoleByName, + getRoleByName: db.getRoleByName, }); if (!hasAccess) { @@ -473,9 +473,9 @@ class AgentClient extends BaseClient { }, }, { - getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, + getConvoFiles: db.getConvoFiles, updateFilesUsage: db.updateFilesUsage, getUserKeyValues: db.getUserKeyValues, getToolFilesByIds: db.getToolFilesByIds, @@ -631,8 +631,8 @@ class AgentClient extends BaseClient { }) { const result = await recordCollectedUsage( { - spendTokens, - spendStructuredTokens, + spendTokens: db.spendTokens, + spendStructuredTokens: db.spendStructuredTokens, pricing: { getMultiplier, getCacheMultiplier }, bulkWriteOps: { insertMany: bulkInsertTransactions, updateBalance }, }, @@ -1134,7 +1134,7 @@ class AgentClient extends BaseClient { context = 'message', }) { try { - await spendTokens( + await db.spendTokens( { model, context, @@ -1153,7 +1153,7 @@ class AgentClient extends BaseClient { 'reasoning_tokens' in usage && typeof usage.reasoning_tokens === 'number' ) { - await spendTokens( + await db.spendTokens( { model, balance, diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 42481e1644..4e3d10e8e6 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -15,13 +15,15 @@ jest.mock('@librechat/api', () => ({ checkAccess: jest.fn(), initializeAgent: jest.fn(), createMemoryProcessor: jest.fn(), -})); - -jest.mock('~/models/Agent', () => ({ loadAgent: jest.fn(), })); -jest.mock('~/models/Role', () => ({ +jest.mock('~/server/services/Config', () => ({ + getMCPServerTools: jest.fn(), +})); + +jest.mock('~/models', () => ({ + getAgent: jest.fn(), getRoleByName: jest.fn(), })); @@ -2138,7 +2140,7 @@ describe('AgentClient - titleConvo', () => { }; mockCheckAccess = require('@librechat/api').checkAccess; - mockLoadAgent = require('~/models/Agent').loadAgent; + mockLoadAgent = require('@librechat/api').loadAgent; mockInitializeAgent = require('@librechat/api').initializeAgent; mockCreateMemoryProcessor = require('@librechat/api').createMemoryProcessor; }); @@ -2195,6 +2197,7 @@ describe('AgentClient - titleConvo', () => { expect.objectContaining({ agent_id: differentAgentId, }), + expect.any(Object), ); expect(mockInitializeAgent).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/api/server/controllers/agents/errors.js b/api/server/controllers/agents/errors.js index 54b296a5d2..b16ce75591 100644 --- a/api/server/controllers/agents/errors.js +++ b/api/server/controllers/agents/errors.js @@ -3,8 +3,8 @@ const { logger } = require('@librechat/data-schemas'); const { CacheKeys, ViolationTypes } = require('librechat-data-provider'); const { sendResponse } = require('~/server/middleware/error'); const { recordUsage } = require('~/server/services/Threads'); -const { getConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); +const { getConvo } = require('~/models'); /** * @typedef {Object} ErrorHandlerContext diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index 189cb29d8d..f1b199dede 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -24,10 +24,7 @@ const { const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); const { createToolEndCallback } = require('~/server/controllers/agents/callbacks'); const { findAccessibleResources } = require('~/server/services/PermissionService'); -const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); -const { getConvoFiles, getConvo } = require('~/models/Conversation'); -const { getAgent, getAgents } = require('~/models/Agent'); const db = require('~/models'); /** @@ -139,7 +136,7 @@ const OpenAIChatCompletionController = async (req, res) => { const agentId = request.model; // Look up the agent - const agent = await getAgent({ id: agentId }); + const agent = await db.getAgent({ id: agentId }); if (!agent) { return sendErrorResponse( res, @@ -221,7 +218,7 @@ const OpenAIChatCompletionController = async (req, res) => { isInitialAgent: true, }, { - getConvoFiles, + getConvoFiles: db.getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, getMessages: db.getMessages, @@ -511,8 +508,8 @@ const OpenAIChatCompletionController = async (req, res) => { const transactionsConfig = getTransactionsConfig(appConfig); recordCollectedUsage( { - spendTokens, - spendStructuredTokens, + spendTokens: db.spendTokens, + spendStructuredTokens: db.spendStructuredTokens, pricing: { getMultiplier, getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, @@ -627,7 +624,7 @@ const ListModelsController = async (req, res) => { // Get the accessible agents let agents = []; if (accessibleAgentIds.length > 0) { - agents = await getAgents({ _id: { $in: accessibleAgentIds } }); + agents = await db.getAgents({ _id: { $in: accessibleAgentIds } }); } const models = agents.map((agent) => ({ @@ -670,7 +667,7 @@ const GetModelController = async (req, res) => { return sendErrorResponse(res, 401, 'Authentication required', 'auth_error'); } - const agent = await getAgent({ id: model }); + const agent = await db.getAgent({ id: model }); if (!agent) { return sendErrorResponse( diff --git a/api/server/controllers/agents/recordCollectedUsage.spec.js b/api/server/controllers/agents/recordCollectedUsage.spec.js index 21720023ca..2d4730c603 100644 --- a/api/server/controllers/agents/recordCollectedUsage.spec.js +++ b/api/server/controllers/agents/recordCollectedUsage.spec.js @@ -18,7 +18,7 @@ const mockRecordCollectedUsage = jest .fn() .mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); -jest.mock('~/models/spendTokens', () => ({ +jest.mock('~/models', () => ({ spendTokens: (...args) => mockSpendTokens(...args), spendStructuredTokens: (...args) => mockSpendStructuredTokens(...args), })); diff --git a/api/server/controllers/agents/request.js b/api/server/controllers/agents/request.js index dea5400036..6f7e1b88c1 100644 --- a/api/server/controllers/agents/request.js +++ b/api/server/controllers/agents/request.js @@ -131,9 +131,15 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit partialMessage.agent_id = req.body.agent_id; } - await saveMessage(req, partialMessage, { - context: 'api/server/controllers/agents/request.js - partial response on disconnect', - }); + await saveMessage( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, + partialMessage, + { context: 'api/server/controllers/agents/request.js - partial response on disconnect' }, + ); logger.debug( `[ResumableAgentController] Saved partial response for ${streamId}, content parts: ${aggregatedContent.length}`, @@ -271,8 +277,14 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit // Save user message BEFORE sending final event to avoid race condition // where client refetch happens before database is updated + const reqCtx = { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }; + if (!client.skipSaveUserMessage && userMessage) { - await saveMessage(req, userMessage, { + await saveMessage(reqCtx, userMessage, { context: 'api/server/controllers/agents/request.js - resumable user message', }); } @@ -282,7 +294,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit // before the response is saved to the database, causing orphaned parentMessageIds. if (client.savedMessageIds && !client.savedMessageIds.has(messageId)) { await saveMessage( - req, + reqCtx, { ...response, user: userId, unfinished: wasAbortedBeforeComplete }, { context: 'api/server/controllers/agents/request.js - resumable response end' }, ); @@ -661,7 +673,11 @@ const _LegacyAgentController = async (req, res, next, initializeClient, addTitle // Save the message if needed if (client.savedMessageIds && !client.savedMessageIds.has(messageId)) { await saveMessage( - req, + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { ...finalResponse, user: userId }, { context: 'api/server/controllers/agents/request.js - response end' }, ); @@ -690,9 +706,15 @@ const _LegacyAgentController = async (req, res, next, initializeClient, addTitle // Save user message if needed if (!client.skipSaveUserMessage) { - await saveMessage(req, userMessage, { - context: "api/server/controllers/agents/request.js - don't skip saving user message", - }); + await saveMessage( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, + userMessage, + { context: "api/server/controllers/agents/request.js - don't skip saving user message" }, + ); } // Add title if needed - extract minimal data diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index 30ccacdba8..de185f4c2b 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -36,10 +36,7 @@ const { } = require('~/server/controllers/agents/callbacks'); const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); const { findAccessibleResources } = require('~/server/services/PermissionService'); -const { getConvoFiles, saveConvo, getConvo } = require('~/models/Conversation'); -const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); -const { getAgent, getAgents } = require('~/models/Agent'); const db = require('~/models'); /** @type {import('@librechat/api').AppConfig | null} */ @@ -214,8 +211,12 @@ async function saveResponseOutput(req, conversationId, responseId, response, age * @returns {Promise} */ async function saveConversation(req, conversationId, agentId, agent) { - await saveConvo( - req, + await db.saveConvo( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { conversationId, endpoint: EModelEndpoint.agents, @@ -279,7 +280,7 @@ const createResponse = async (req, res) => { const isStreaming = request.stream === true; // Look up the agent - const agent = await getAgent({ id: agentId }); + const agent = await db.getAgent({ id: agentId }); if (!agent) { return sendResponsesErrorResponse( res, @@ -355,7 +356,7 @@ const createResponse = async (req, res) => { isInitialAgent: true, }, { - getConvoFiles, + getConvoFiles: db.getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, getMessages: db.getMessages, @@ -525,8 +526,8 @@ const createResponse = async (req, res) => { const transactionsConfig = getTransactionsConfig(req.config); recordCollectedUsage( { - spendTokens, - spendStructuredTokens, + spendTokens: db.spendTokens, + spendStructuredTokens: db.spendStructuredTokens, pricing: { getMultiplier, getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, @@ -680,8 +681,8 @@ const createResponse = async (req, res) => { const transactionsConfig = getTransactionsConfig(req.config); recordCollectedUsage( { - spendTokens, - spendStructuredTokens, + spendTokens: db.spendTokens, + spendStructuredTokens: db.spendStructuredTokens, pricing: { getMultiplier, getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, @@ -782,7 +783,7 @@ const listModels = async (req, res) => { // Get the accessible agents let agents = []; if (accessibleAgentIds.length > 0) { - agents = await getAgents({ _id: { $in: accessibleAgentIds } }); + agents = await db.getAgents({ _id: { $in: accessibleAgentIds } }); } // Convert to models format @@ -832,7 +833,7 @@ const getResponse = async (req, res) => { // The responseId could be either the response ID or the conversation ID // Try to find a conversation with this ID - const conversation = await getConvo(userId, responseId); + const conversation = await db.getConvo(userId, responseId); if (!conversation) { return sendResponsesErrorResponse( diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 899b561352..40d80d571f 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -25,15 +25,6 @@ const { actionDelimiter, removeNullishValues, } = require('librechat-data-provider'); -const { - getListAgentsByAccess, - countPromotedAgents, - revertAgentVersion, - createAgent, - updateAgent, - deleteAgent, - getAgent, -} = require('~/models/Agent'); const { findPubliclyAccessibleResources, getResourcePermissionsMap, @@ -42,15 +33,14 @@ const { grantPermission, } = require('~/server/services/PermissionService'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { getCategoriesWithCounts, deleteFileByFilter } = require('~/models'); const { resizeAvatar } = require('~/server/services/Files/images/avatar'); const { getFileStrategy } = require('~/server/utils/getFileStrategy'); const { refreshS3Url } = require('~/server/services/Files/S3/crud'); const { filterFile } = require('~/server/services/Files/process'); -const { updateAction, getActions } = require('~/models/Action'); const { getCachedTools } = require('~/server/services/Config'); const { getMCPServersRegistry } = require('~/config'); const { getLogStores } = require('~/cache'); +const db = require('~/models'); const systemTools = { [Tools.execute_code]: true, @@ -207,7 +197,7 @@ const createAgentHandler = async (req, res) => { const availableTools = (await getCachedTools()) ?? {}; agentData.tools = await filterAuthorizedTools({ tools, userId, availableTools }); - const agent = await createAgent(agentData); + const agent = await db.createAgent(agentData); try { await Promise.all([ @@ -267,7 +257,7 @@ const getAgentHandler = async (req, res, expandProperties = false) => { // Permissions are validated by middleware before calling this function // Simply load the agent by ID - const agent = await getAgent({ id }); + const agent = await db.getAgent({ id }); if (!agent) { return res.status(404).json({ error: 'Agent not found' }); @@ -366,7 +356,7 @@ const updateAgentHandler = async (req, res) => { // Convert OCR to context in incoming updateData convertOcrToContextInPlace(updateData); - const existingAgent = await getAgent({ id }); + const existingAgent = await db.getAgent({ id }); if (!existingAgent) { return res.status(404).json({ error: 'Agent not found' }); @@ -403,7 +393,7 @@ const updateAgentHandler = async (req, res) => { let updatedAgent = Object.keys(updateData).length > 0 - ? await updateAgent({ id }, updateData, { + ? await db.updateAgent({ id }, updateData, { updatingUserId: req.user.id, }) : existingAgent; @@ -453,7 +443,7 @@ const duplicateAgentHandler = async (req, res) => { const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret']; try { - const agent = await getAgent({ id }); + const agent = await db.getAgent({ id }); if (!agent) { return res.status(404).json({ error: 'Agent not found', @@ -501,7 +491,7 @@ const duplicateAgentHandler = async (req, res) => { }); const newActionsList = []; - const originalActions = (await getActions({ agent_id: id }, true)) ?? []; + const originalActions = (await db.getActions({ agent_id: id }, true)) ?? []; const promises = []; /** @@ -520,7 +510,7 @@ const duplicateAgentHandler = async (req, res) => { delete filteredMetadata[field]; } - const newAction = await updateAction( + const newAction = await db.updateAction( { action_id: newActionId, agent_id: newAgentId }, { metadata: filteredMetadata, @@ -554,7 +544,7 @@ const duplicateAgentHandler = async (req, res) => { }); } - const newAgent = await createAgent(newAgentData); + const newAgent = await db.createAgent(newAgentData); try { await Promise.all([ @@ -607,11 +597,11 @@ const duplicateAgentHandler = async (req, res) => { const deleteAgentHandler = async (req, res) => { try { const id = req.params.id; - const agent = await getAgent({ id }); + const agent = await db.getAgent({ id }); if (!agent) { return res.status(404).json({ error: 'Agent not found' }); } - await deleteAgent({ id }); + await db.deleteAgent({ id }); return res.json({ message: 'Agent deleted' }); } catch (error) { logger.error('[/Agents/:id] Error deleting Agent', error); @@ -686,7 +676,7 @@ const getListAgentsHandler = async (req, res) => { cachedRefresh != null && typeof cachedRefresh === 'object' && cachedRefresh.urlCache != null; if (!isValidCachedRefresh) { try { - const fullList = await getListAgentsByAccess({ + const fullList = await db.getListAgentsByAccess({ accessibleIds, otherParams: {}, limit: MAX_AVATAR_REFRESH_AGENTS, @@ -696,7 +686,7 @@ const getListAgentsHandler = async (req, res) => { agents: fullList?.data ?? [], userId, refreshS3Url, - updateAgent, + updateAgent: db.updateAgent, }); cachedRefresh = { urlCache }; await cache.set(refreshKey, cachedRefresh, Time.THIRTY_MINUTES); @@ -708,7 +698,7 @@ const getListAgentsHandler = async (req, res) => { } // Use the new ACL-aware function - const data = await getListAgentsByAccess({ + const data = await db.getListAgentsByAccess({ accessibleIds, otherParams: filter, limit, @@ -773,7 +763,7 @@ const uploadAgentAvatarHandler = async (req, res) => { return res.status(400).json({ message: 'Agent ID is required' }); } - const existingAgent = await getAgent({ id: agent_id }); + const existingAgent = await db.getAgent({ id: agent_id }); if (!existingAgent) { return res.status(404).json({ error: 'Agent not found' }); @@ -805,7 +795,7 @@ const uploadAgentAvatarHandler = async (req, res) => { const { deleteFile } = getStrategyFunctions(_avatar.source); try { await deleteFile(req, { filepath: _avatar.filepath }); - await deleteFileByFilter({ user: req.user.id, filepath: _avatar.filepath }); + await db.deleteFileByFilter({ user: req.user.id, filepath: _avatar.filepath }); } catch (error) { logger.error('[/:agent_id/avatar] Error deleting old avatar', error); } @@ -818,7 +808,7 @@ const uploadAgentAvatarHandler = async (req, res) => { }, }; - const updatedAgent = await updateAgent({ id: agent_id }, data, { + const updatedAgent = await db.updateAgent({ id: agent_id }, data, { updatingUserId: req.user.id, }); @@ -874,7 +864,7 @@ const revertAgentVersionHandler = async (req, res) => { return res.status(400).json({ error: 'version_index is required' }); } - const existingAgent = await getAgent({ id }); + const existingAgent = await db.getAgent({ id }); if (!existingAgent) { return res.status(404).json({ error: 'Agent not found' }); @@ -882,7 +872,7 @@ const revertAgentVersionHandler = async (req, res) => { // Permissions are enforced via route middleware (ACL EDIT) - let updatedAgent = await revertAgentVersion({ id }, version_index); + let updatedAgent = await db.revertAgentVersion({ id }, version_index); if (updatedAgent.tools?.length) { const availableTools = (await getCachedTools()) ?? {}; @@ -893,7 +883,7 @@ const revertAgentVersionHandler = async (req, res) => { existingTools: updatedAgent.tools, }); if (filteredTools.length !== updatedAgent.tools.length) { - updatedAgent = await updateAgent( + updatedAgent = await db.updateAgent( { id }, { tools: filteredTools }, { updatingUserId: req.user.id }, @@ -923,8 +913,8 @@ const revertAgentVersionHandler = async (req, res) => { */ const getAgentCategories = async (_req, res) => { try { - const categories = await getCategoriesWithCounts(); - const promotedCount = await countPromotedAgents(); + const categories = await db.getCategoriesWithCounts(); + const promotedCount = await db.countPromotedAgents(); const formattedCategories = categories.map((category) => ({ value: category.value, label: category.label, diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 56bb90675a..9a8dd0a50a 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -30,15 +30,6 @@ jest.mock('~/server/services/Files/process', () => ({ filterFile: jest.fn(), })); -jest.mock('~/models/Action', () => ({ - updateAction: jest.fn(), - getActions: jest.fn().mockResolvedValue([]), -})); - -jest.mock('~/models/File', () => ({ - deleteFileByFilter: jest.fn(), -})); - jest.mock('~/server/services/PermissionService', () => ({ findAccessibleResources: jest.fn().mockResolvedValue([]), findPubliclyAccessibleResources: jest.fn().mockResolvedValue([]), @@ -47,9 +38,18 @@ jest.mock('~/server/services/PermissionService', () => ({ hasPublicPermission: jest.fn().mockResolvedValue(false), })); -jest.mock('~/models', () => ({ - getCategoriesWithCounts: jest.fn(), -})); +jest.mock('~/models', () => { + const mongoose = require('mongoose'); + const { createMethods } = require('@librechat/data-schemas'); + const methods = createMethods(mongoose, { + removeAllPermissions: jest.fn().mockResolvedValue(undefined), + }); + return { + ...methods, + getCategoriesWithCounts: jest.fn(), + deleteFileByFilter: jest.fn(), + }; +}); // Mock cache for S3 avatar refresh tests const mockCache = { diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index 804594d0bf..e4a20c2a5e 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -1,7 +1,13 @@ const { v4 } = require('uuid'); const { sleep } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); -const { sendEvent, getBalanceConfig, getModelMaxTokens, countTokens } = require('@librechat/api'); +const { + sendEvent, + countTokens, + checkBalance, + getBalanceConfig, + getModelMaxTokens, +} = require('@librechat/api'); const { Time, Constants, @@ -31,10 +37,14 @@ const { createRun, StreamRunManager } = require('~/server/services/Runs'); const { addTitle } = require('~/server/services/Endpoints/assistants'); const { createRunBody } = require('~/server/services/createRunBody'); const { sendResponse } = require('~/server/middleware/error'); -const { getTransactions } = require('~/models/Transaction'); -const { checkBalance } = require('~/models/balanceMethods'); -const { getConvo } = require('~/models/Conversation'); -const getLogStores = require('~/cache/getLogStores'); +const { + createAutoRefillTransaction, + findBalanceByUser, + getTransactions, + getMultiplier, + getConvo, +} = require('~/models'); +const { logViolation, getLogStores } = require('~/cache'); const { getOpenAIClient } = require('./helpers'); /** @@ -275,16 +285,19 @@ const chatV1 = async (req, res) => { // Count tokens up to the current context window promptTokens = Math.min(promptTokens, getModelMaxTokens(model)); - await checkBalance({ - req, - res, - txData: { - model, - user: req.user.id, - tokenType: 'prompt', - amount: promptTokens, + await checkBalance( + { + req, + res, + txData: { + model, + user: req.user.id, + tokenType: 'prompt', + amount: promptTokens, + }, }, - }); + { findBalanceByUser, getMultiplier, createAutoRefillTransaction, logViolation }, + ); }; const { openai: _openai } = await getOpenAIClient({ diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index 414681d6dc..559d9d8953 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -1,7 +1,13 @@ const { v4 } = require('uuid'); const { sleep } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); -const { sendEvent, getBalanceConfig, getModelMaxTokens, countTokens } = require('@librechat/api'); +const { + sendEvent, + countTokens, + checkBalance, + getBalanceConfig, + getModelMaxTokens, +} = require('@librechat/api'); const { Time, Constants, @@ -26,10 +32,14 @@ const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { createRun, StreamRunManager } = require('~/server/services/Runs'); const { addTitle } = require('~/server/services/Endpoints/assistants'); const { createRunBody } = require('~/server/services/createRunBody'); -const { getTransactions } = require('~/models/Transaction'); -const { checkBalance } = require('~/models/balanceMethods'); -const { getConvo } = require('~/models/Conversation'); -const getLogStores = require('~/cache/getLogStores'); +const { + getConvo, + getMultiplier, + getTransactions, + findBalanceByUser, + createAutoRefillTransaction, +} = require('~/models'); +const { logViolation, getLogStores } = require('~/cache'); const { getOpenAIClient } = require('./helpers'); /** @@ -148,16 +158,19 @@ const chatV2 = async (req, res) => { // Count tokens up to the current context window promptTokens = Math.min(promptTokens, getModelMaxTokens(model)); - await checkBalance({ - req, - res, - txData: { - model, - user: req.user.id, - tokenType: 'prompt', - amount: promptTokens, + await checkBalance( + { + req, + res, + txData: { + model, + user: req.user.id, + tokenType: 'prompt', + amount: promptTokens, + }, }, - }); + { findBalanceByUser, getMultiplier, createAutoRefillTransaction, logViolation }, + ); }; const { openai: _openai } = await getOpenAIClient({ diff --git a/api/server/controllers/assistants/errors.js b/api/server/controllers/assistants/errors.js index 1ae12ea3d5..f8dcf39f2b 100644 --- a/api/server/controllers/assistants/errors.js +++ b/api/server/controllers/assistants/errors.js @@ -3,8 +3,8 @@ const { logger } = require('@librechat/data-schemas'); const { CacheKeys, ViolationTypes, ContentTypes } = require('librechat-data-provider'); const { recordUsage, checkMessageGaps } = require('~/server/services/Threads'); const { sendResponse } = require('~/server/middleware/error'); -const { getConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); +const { getConvo } = require('~/models'); /** * @typedef {Object} ErrorHandlerContext diff --git a/api/server/controllers/assistants/v1.js b/api/server/controllers/assistants/v1.js index 5d13d30334..c441b7ec59 100644 --- a/api/server/controllers/assistants/v1.js +++ b/api/server/controllers/assistants/v1.js @@ -1,15 +1,14 @@ const fs = require('fs').promises; const { logger } = require('@librechat/data-schemas'); const { FileContext } = require('librechat-data-provider'); +const { deleteFileByFilter, updateAssistantDoc, getAssistants } = require('~/models'); const { uploadImageBuffer, filterFile } = require('~/server/services/Files/process'); const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { deleteAssistantActions } = require('~/server/services/ActionService'); -const { updateAssistantDoc, getAssistants } = require('~/models/Assistant'); const { getOpenAIClient, fetchAssistants } = require('./helpers'); const { getCachedTools } = require('~/server/services/Config'); const { manifestToolMap } = require('~/app/clients/tools'); -const { deleteFileByFilter } = require('~/models'); /** * Create an assistant. diff --git a/api/server/controllers/assistants/v2.js b/api/server/controllers/assistants/v2.js index b9c5cd709f..cc0e03916d 100644 --- a/api/server/controllers/assistants/v2.js +++ b/api/server/controllers/assistants/v2.js @@ -3,8 +3,8 @@ const { ToolCallTypes } = require('librechat-data-provider'); const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { validateAndUpdateTool } = require('~/server/services/ActionService'); const { getCachedTools } = require('~/server/services/Config'); -const { updateAssistantDoc } = require('~/models/Assistant'); const { manifestToolMap } = require('~/app/clients/tools'); +const { updateAssistantDoc } = require('~/models'); const { getOpenAIClient } = require('./helpers'); /** diff --git a/api/server/controllers/tools.js b/api/server/controllers/tools.js index 14a757e2bc..1df11b1059 100644 --- a/api/server/controllers/tools.js +++ b/api/server/controllers/tools.js @@ -9,13 +9,11 @@ const { ToolCallTypes, PermissionTypes, } = require('librechat-data-provider'); +const { getRoleByName, createToolCall, getToolCallsByConvo, getMessage } = require('~/models'); const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process'); const { processCodeOutput } = require('~/server/services/Files/Code/process'); -const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { loadTools } = require('~/app/clients/tools/util'); -const { getRoleByName } = require('~/models/Role'); -const { getMessage } = require('~/models/Message'); const fieldsMap = { [Tools.execute_code]: [EnvVar.CODE_API_KEY], diff --git a/api/server/experimental.js b/api/server/experimental.js index 7b60ad7fd2..8982b69afb 100644 --- a/api/server/experimental.js +++ b/api/server/experimental.js @@ -24,14 +24,14 @@ const { connectDb, indexSync } = require('~/db'); const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager'); const createValidateImageRequest = require('./middleware/validateImageRequest'); const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies'); -const { updateInterfacePermissions } = require('~/models/interface'); +const { updateInterfacePermissions: updateInterfacePerms } = require('@librechat/api'); +const { getRoleByName, updateAccessPermissions, seedDatabase } = require('~/models'); const { checkMigrations } = require('./services/start/migration'); const initializeMCPs = require('./services/initializeMCPs'); const configureSocialLogins = require('./socialLogins'); const { getAppConfig } = require('./services/Config'); const staticCache = require('./utils/staticCache'); const noIndex = require('./middleware/noIndex'); -const { seedDatabase } = require('~/models'); const routes = require('./routes'); const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {}; @@ -222,7 +222,7 @@ if (cluster.isMaster) { const appConfig = await getAppConfig(); initializeFileStorage(appConfig); await performStartupChecks(appConfig); - await updateInterfacePermissions(appConfig); + await updateInterfacePerms({ appConfig, getRoleByName, updateAccessPermissions }); /** Load index.html for SPA serving */ const indexPath = path.join(appConfig.paths.dist, 'index.html'); diff --git a/api/server/index.js b/api/server/index.js index f034f10236..6af829eab8 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -25,14 +25,14 @@ const { connectDb, indexSync } = require('~/db'); const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager'); const createValidateImageRequest = require('./middleware/validateImageRequest'); const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies'); -const { updateInterfacePermissions } = require('~/models/interface'); +const { updateInterfacePermissions: updateInterfacePerms } = require('@librechat/api'); +const { getRoleByName, updateAccessPermissions, seedDatabase } = require('~/models'); const { checkMigrations } = require('./services/start/migration'); const initializeMCPs = require('./services/initializeMCPs'); const configureSocialLogins = require('./socialLogins'); const { getAppConfig } = require('./services/Config'); const staticCache = require('./utils/staticCache'); const noIndex = require('./middleware/noIndex'); -const { seedDatabase } = require('~/models'); const routes = require('./routes'); const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {}; @@ -62,7 +62,7 @@ const startServer = async () => { const appConfig = await getAppConfig(); initializeFileStorage(appConfig); await performStartupChecks(appConfig); - await updateInterfacePermissions(appConfig); + await updateInterfacePerms({ appConfig, getRoleByName, updateAccessPermissions }); const indexPath = path.join(appConfig.paths.dist, 'index.html'); let indexHTML = fs.readFileSync(indexPath, 'utf8'); diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index d39b0104a8..624ace7f9f 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -1,4 +1,5 @@ const { logger } = require('@librechat/data-schemas'); +const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider'); const { isEnabled, sendEvent, @@ -9,7 +10,6 @@ const { } = require('@librechat/api'); const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider'); const { saveMessage, getConvo, updateBalance, bulkInsertTransactions } = require('~/models'); -const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); const { truncateText, smartTruncateText } = require('~/app/clients/prompts'); const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); const clearPendingReq = require('~/cache/clearPendingReq'); @@ -130,7 +130,11 @@ async function abortMessage(req, res) { } await saveMessage( - req, + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { ...responseMessage, user: userId }, { context: 'api/server/middleware/abortMiddleware.js' }, ); diff --git a/api/server/middleware/abortMiddleware.spec.js b/api/server/middleware/abortMiddleware.spec.js index 795814a928..c9c0d5cc60 100644 --- a/api/server/middleware/abortMiddleware.spec.js +++ b/api/server/middleware/abortMiddleware.spec.js @@ -20,15 +20,7 @@ const mockRecordCollectedUsage = jest const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); -jest.mock('~/models/spendTokens', () => ({ - spendTokens: (...args) => mockSpendTokens(...args), - spendStructuredTokens: (...args) => mockSpendStructuredTokens(...args), -})); -jest.mock('~/models/tx', () => ({ - getMultiplier: mockGetMultiplier, - getCacheMultiplier: mockGetCacheMultiplier, -})); jest.mock('@librechat/data-schemas', () => ({ logger: { diff --git a/api/server/middleware/abortRun.js b/api/server/middleware/abortRun.js index 44375f5024..318693fe15 100644 --- a/api/server/middleware/abortRun.js +++ b/api/server/middleware/abortRun.js @@ -3,8 +3,7 @@ const { logger } = require('@librechat/data-schemas'); const { CacheKeys, RunStatus, isUUID } = require('librechat-data-provider'); const { initializeClient } = require('~/server/services/Endpoints/assistants'); const { checkMessageGaps, recordUsage } = require('~/server/services/Threads'); -const { deleteMessages } = require('~/models/Message'); -const { getConvo } = require('~/models/Conversation'); +const { deleteMessages, getConvo } = require('~/models'); const getLogStores = require('~/cache/getLogStores'); const three_minutes = 1000 * 60 * 3; diff --git a/api/server/middleware/accessResources/canAccessAgentFromBody.js b/api/server/middleware/accessResources/canAccessAgentFromBody.js index 572a86f5e5..5ade76bb77 100644 --- a/api/server/middleware/accessResources/canAccessAgentFromBody.js +++ b/api/server/middleware/accessResources/canAccessAgentFromBody.js @@ -10,8 +10,9 @@ const { } = require('librechat-data-provider'); const { checkPermission } = require('~/server/services/PermissionService'); const { canAccessResource } = require('./canAccessResource'); -const { getRoleByName } = require('~/models/Role'); -const { getAgent } = require('~/models/Agent'); +const db = require('~/models'); + +const { getRoleByName, getAgent } = db; /** * Resolves custom agent ID (e.g., "agent_abc123") to a MongoDB document. diff --git a/api/server/middleware/accessResources/canAccessAgentResource.js b/api/server/middleware/accessResources/canAccessAgentResource.js index 62d9f248c0..4c00ab4982 100644 --- a/api/server/middleware/accessResources/canAccessAgentResource.js +++ b/api/server/middleware/accessResources/canAccessAgentResource.js @@ -1,6 +1,6 @@ const { ResourceType } = require('librechat-data-provider'); const { canAccessResource } = require('./canAccessResource'); -const { getAgent } = require('~/models/Agent'); +const { getAgent } = require('~/models'); /** * Agent ID resolver function diff --git a/api/server/middleware/accessResources/canAccessAgentResource.spec.js b/api/server/middleware/accessResources/canAccessAgentResource.spec.js index 1106390e72..786636ee74 100644 --- a/api/server/middleware/accessResources/canAccessAgentResource.spec.js +++ b/api/server/middleware/accessResources/canAccessAgentResource.spec.js @@ -3,7 +3,7 @@ const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data- const { MongoMemoryServer } = require('mongodb-memory-server'); const { canAccessAgentResource } = require('./canAccessAgentResource'); const { User, Role, AclEntry } = require('~/db/models'); -const { createAgent } = require('~/models/Agent'); +const { createAgent } = require('~/models'); describe('canAccessAgentResource middleware', () => { let mongoServer; @@ -373,7 +373,7 @@ describe('canAccessAgentResource middleware', () => { jest.clearAllMocks(); // Update the agent - const { updateAgent } = require('~/models/Agent'); + const { updateAgent } = require('~/models'); await updateAgent({ id: agentId }, { description: 'Updated description' }); // Test edit access diff --git a/api/server/middleware/accessResources/canAccessPromptGroupResource.js b/api/server/middleware/accessResources/canAccessPromptGroupResource.js index 90aa280772..9da1994a77 100644 --- a/api/server/middleware/accessResources/canAccessPromptGroupResource.js +++ b/api/server/middleware/accessResources/canAccessPromptGroupResource.js @@ -1,6 +1,6 @@ const { ResourceType } = require('librechat-data-provider'); const { canAccessResource } = require('./canAccessResource'); -const { getPromptGroup } = require('~/models/Prompt'); +const { getPromptGroup } = require('~/models'); /** * PromptGroup ID resolver function diff --git a/api/server/middleware/accessResources/canAccessPromptViaGroup.js b/api/server/middleware/accessResources/canAccessPromptViaGroup.js index 0bb0a804a9..534db3d6c6 100644 --- a/api/server/middleware/accessResources/canAccessPromptViaGroup.js +++ b/api/server/middleware/accessResources/canAccessPromptViaGroup.js @@ -1,6 +1,6 @@ const { ResourceType } = require('librechat-data-provider'); const { canAccessResource } = require('./canAccessResource'); -const { getPrompt } = require('~/models/Prompt'); +const { getPrompt } = require('~/models'); /** * Prompt to PromptGroup ID resolver function diff --git a/api/server/middleware/accessResources/fileAccess.js b/api/server/middleware/accessResources/fileAccess.js index 25d41e7c02..0f77a61175 100644 --- a/api/server/middleware/accessResources/fileAccess.js +++ b/api/server/middleware/accessResources/fileAccess.js @@ -1,8 +1,7 @@ const { logger } = require('@librechat/data-schemas'); const { PermissionBits, hasPermissions, ResourceType } = require('librechat-data-provider'); const { getEffectivePermissions } = require('~/server/services/PermissionService'); -const { getAgents } = require('~/models/Agent'); -const { getFiles } = require('~/models'); +const { getAgents, getFiles } = require('~/models'); /** * Checks if user has access to a file through agent permissions diff --git a/api/server/middleware/accessResources/fileAccess.spec.js b/api/server/middleware/accessResources/fileAccess.spec.js index cc0d57ac48..72896b0629 100644 --- a/api/server/middleware/accessResources/fileAccess.spec.js +++ b/api/server/middleware/accessResources/fileAccess.spec.js @@ -3,8 +3,7 @@ const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data- const { MongoMemoryServer } = require('mongodb-memory-server'); const { fileAccess } = require('./fileAccess'); const { User, Role, AclEntry } = require('~/db/models'); -const { createAgent } = require('~/models/Agent'); -const { createFile } = require('~/models'); +const { createAgent, createFile } = require('~/models'); describe('fileAccess middleware', () => { let mongoServer; diff --git a/api/server/middleware/assistants/validateAuthor.js b/api/server/middleware/assistants/validateAuthor.js index 03936444e0..6c15704251 100644 --- a/api/server/middleware/assistants/validateAuthor.js +++ b/api/server/middleware/assistants/validateAuthor.js @@ -1,5 +1,5 @@ const { SystemRoles } = require('librechat-data-provider'); -const { getAssistant } = require('~/models/Assistant'); +const { getAssistant } = require('~/models'); /** * Checks if the assistant is supported or excluded diff --git a/api/server/middleware/checkInviteUser.js b/api/server/middleware/checkInviteUser.js index 42e1faba5b..22f2824ffc 100644 --- a/api/server/middleware/checkInviteUser.js +++ b/api/server/middleware/checkInviteUser.js @@ -1,5 +1,8 @@ -const { getInvite } = require('~/models/inviteUser'); -const { deleteTokens } = require('~/models'); +const { getInvite: getInviteFn } = require('@librechat/api'); +const { createToken, findToken, deleteTokens } = require('~/models'); + +const getInvite = (encodedToken, email) => + getInviteFn(encodedToken, email, { createToken, findToken }); async function checkInviteUser(req, res, next) { const token = req.body.token; diff --git a/api/server/middleware/checkPeoplePickerAccess.js b/api/server/middleware/checkPeoplePickerAccess.js index af2154dbba..50f137285e 100644 --- a/api/server/middleware/checkPeoplePickerAccess.js +++ b/api/server/middleware/checkPeoplePickerAccess.js @@ -1,6 +1,6 @@ const { logger } = require('@librechat/data-schemas'); const { PrincipalType, PermissionTypes, Permissions } = require('librechat-data-provider'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); const VALID_PRINCIPAL_TYPES = new Set([ PrincipalType.USER, diff --git a/api/server/middleware/checkPeoplePickerAccess.spec.js b/api/server/middleware/checkPeoplePickerAccess.spec.js index 9a229610de..c394bbae65 100644 --- a/api/server/middleware/checkPeoplePickerAccess.spec.js +++ b/api/server/middleware/checkPeoplePickerAccess.spec.js @@ -1,9 +1,9 @@ const { logger } = require('@librechat/data-schemas'); const { PrincipalType, PermissionTypes, Permissions } = require('librechat-data-provider'); const { checkPeoplePickerAccess } = require('./checkPeoplePickerAccess'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); -jest.mock('~/models/Role'); +jest.mock('~/models'); jest.mock('@librechat/data-schemas', () => ({ ...jest.requireActual('@librechat/data-schemas'), logger: { diff --git a/api/server/middleware/checkSharePublicAccess.js b/api/server/middleware/checkSharePublicAccess.js index 0e95b9f6f8..c7b65a077e 100644 --- a/api/server/middleware/checkSharePublicAccess.js +++ b/api/server/middleware/checkSharePublicAccess.js @@ -1,6 +1,6 @@ const { logger } = require('@librechat/data-schemas'); const { ResourceType, PermissionTypes, Permissions } = require('librechat-data-provider'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); /** * Maps resource types to their corresponding permission types diff --git a/api/server/middleware/checkSharePublicAccess.spec.js b/api/server/middleware/checkSharePublicAccess.spec.js index c73e71693b..605de2049e 100644 --- a/api/server/middleware/checkSharePublicAccess.spec.js +++ b/api/server/middleware/checkSharePublicAccess.spec.js @@ -1,8 +1,8 @@ const { ResourceType, PermissionTypes, Permissions } = require('librechat-data-provider'); const { checkSharePublicAccess } = require('./checkSharePublicAccess'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); -jest.mock('~/models/Role'); +jest.mock('~/models'); describe('checkSharePublicAccess middleware', () => { let mockReq; diff --git a/api/server/middleware/denyRequest.js b/api/server/middleware/denyRequest.js index 20360519cf..86054d0a23 100644 --- a/api/server/middleware/denyRequest.js +++ b/api/server/middleware/denyRequest.js @@ -43,7 +43,11 @@ const denyRequest = async (req, res, errorMessage) => { if (shouldSaveMessage) { await saveMessage( - req, + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { ...userMessage, user: req.user.id }, { context: `api/server/middleware/denyRequest.js - ${responseText}` }, ); diff --git a/api/server/middleware/error.js b/api/server/middleware/error.js index fef7e60ef7..5fa3562c30 100644 --- a/api/server/middleware/error.js +++ b/api/server/middleware/error.js @@ -2,8 +2,7 @@ const crypto = require('crypto'); const { logger } = require('@librechat/data-schemas'); const { parseConvo } = require('librechat-data-provider'); const { sendEvent, handleError, sanitizeMessageForTransmit } = require('@librechat/api'); -const { saveMessage, getMessages } = require('~/models/Message'); -const { getConvo } = require('~/models/Conversation'); +const { saveMessage, getMessages, getConvo } = require('~/models'); /** * Processes an error with provided options, saves the error message and sends a corresponding SSE response @@ -49,7 +48,11 @@ const sendError = async (req, res, options, callback) => { if (shouldSaveMessage) { await saveMessage( - req, + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { ...errorMessage, user }, { context: 'api/server/utils/streamResponse.js - sendError', diff --git a/api/server/middleware/roles/access.spec.js b/api/server/middleware/roles/access.spec.js index 9de840819d..16fb6df138 100644 --- a/api/server/middleware/roles/access.spec.js +++ b/api/server/middleware/roles/access.spec.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { checkAccess, generateCheckAccess } = require('@librechat/api'); const { PermissionTypes, Permissions } = require('librechat-data-provider'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); const { Role } = require('~/db/models'); // Mock the logger from @librechat/data-schemas diff --git a/api/server/middleware/validate/convoAccess.js b/api/server/middleware/validate/convoAccess.js index 127bfdc530..ef1eea8f37 100644 --- a/api/server/middleware/validate/convoAccess.js +++ b/api/server/middleware/validate/convoAccess.js @@ -1,8 +1,8 @@ const { isEnabled } = require('@librechat/api'); const { Constants, ViolationTypes, Time } = require('librechat-data-provider'); -const { searchConversation } = require('~/models/Conversation'); const denyRequest = require('~/server/middleware/denyRequest'); const { logViolation, getLogStores } = require('~/cache'); +const { searchConversation } = require('~/models'); const { USE_REDIS, CONVO_ACCESS_VIOLATION_SCORE: score = 0 } = process.env ?? {}; diff --git a/api/server/routes/__tests__/convos.spec.js b/api/server/routes/__tests__/convos.spec.js index 3bdeac32db..23978f28e9 100644 --- a/api/server/routes/__tests__/convos.spec.js +++ b/api/server/routes/__tests__/convos.spec.js @@ -7,8 +7,6 @@ jest.mock('@librechat/agents', () => require(MOCKS).agents()); jest.mock('@librechat/api', () => require(MOCKS).api()); jest.mock('@librechat/data-schemas', () => require(MOCKS).dataSchemas()); jest.mock('librechat-data-provider', () => require(MOCKS).dataProvider()); -jest.mock('~/models/Conversation', () => require(MOCKS).conversationModel()); -jest.mock('~/models/ToolCall', () => require(MOCKS).toolCallModel()); jest.mock('~/models', () => require(MOCKS).sharedModels()); jest.mock('~/server/middleware/requireJwtAuth', () => require(MOCKS).requireJwtAuth()); jest.mock('~/server/middleware', () => require(MOCKS).middlewarePassthrough()); @@ -23,9 +21,13 @@ jest.mock('~/server/services/Endpoints/assistants', () => require(MOCKS).assista describe('Convos Routes', () => { let app; let convosRouter; - const { deleteAllSharedLinks, deleteConvoSharedLink } = require('~/models'); - const { deleteConvos, saveConvo } = require('~/models/Conversation'); - const { deleteToolCalls } = require('~/models/ToolCall'); + const { + deleteAllSharedLinks, + deleteConvoSharedLink, + deleteToolCalls, + deleteConvos, + saveConvo, + } = require('~/models'); beforeAll(() => { convosRouter = require('../convos'); @@ -435,7 +437,7 @@ describe('Convos Routes', () => { expect(response.status).toBe(200); expect(response.body).toEqual(mockArchivedConvo); expect(saveConvo).toHaveBeenCalledWith( - expect.objectContaining({ user: { id: 'test-user-123' } }), + expect.objectContaining({ userId: 'test-user-123' }), { conversationId: mockConversationId, isArchived: true }, { context: `POST /api/convos/archive ${mockConversationId}` }, ); @@ -464,7 +466,7 @@ describe('Convos Routes', () => { expect(response.status).toBe(200); expect(response.body).toEqual(mockUnarchivedConvo); expect(saveConvo).toHaveBeenCalledWith( - expect.objectContaining({ user: { id: 'test-user-123' } }), + expect.objectContaining({ userId: 'test-user-123' }), { conversationId: mockConversationId, isArchived: false }, { context: `POST /api/convos/archive ${mockConversationId}` }, ); diff --git a/api/server/routes/accessPermissions.test.js b/api/server/routes/accessPermissions.test.js index 81c21c8667..ddbe702f15 100644 --- a/api/server/routes/accessPermissions.test.js +++ b/api/server/routes/accessPermissions.test.js @@ -5,7 +5,7 @@ const { v4: uuidv4 } = require('uuid'); const { createMethods } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { ResourceType, PermissionBits } = require('librechat-data-provider'); -const { createAgent } = require('~/models/Agent'); +const { createAgent } = require('~/models'); /** * Mock the PermissionsController to isolate route testing diff --git a/api/server/routes/admin/auth.js b/api/server/routes/admin/auth.js index 291b5eaaf8..e729f20940 100644 --- a/api/server/routes/admin/auth.js +++ b/api/server/routes/admin/auth.js @@ -11,15 +11,16 @@ const { } = require('@librechat/api'); const { loginController } = require('~/server/controllers/auth/LoginController'); const { createOAuthHandler } = require('~/server/controllers/auth/oauth'); +const { findBalanceByUser, upsertBalanceFields } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); const getLogStores = require('~/cache/getLogStores'); const { getOpenIdConfig } = require('~/strategies'); const middleware = require('~/server/middleware'); -const { Balance } = require('~/db/models'); const setBalanceConfig = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const router = express.Router(); diff --git a/api/server/routes/agents/actions.js b/api/server/routes/agents/actions.js index f3970bff22..b3b34f3f1c 100644 --- a/api/server/routes/agents/actions.js +++ b/api/server/routes/agents/actions.js @@ -18,17 +18,15 @@ const { domainParser, } = require('~/server/services/ActionService'); const { findAccessibleResources } = require('~/server/services/PermissionService'); -const { getAgent, updateAgent, getListAgentsByAccess } = require('~/models/Agent'); -const { updateAction, getActions, deleteAction } = require('~/models/Action'); +const db = require('~/models'); const { canAccessAgentResource } = require('~/server/middleware'); -const { getRoleByName } = require('~/models/Role'); const router = express.Router(); const checkAgentCreate = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE, Permissions.CREATE], - getRoleByName, + getRoleByName: db.getRoleByName, }); /** @@ -47,13 +45,15 @@ router.get('/', async (req, res) => { requiredPermissions: PermissionBits.EDIT, }); - const agentsResponse = await getListAgentsByAccess({ + const agentsResponse = await db.getListAgentsByAccess({ accessibleIds: editableAgentObjectIds, }); const editableAgentIds = agentsResponse.data.map((agent) => agent.id); const actions = - editableAgentIds.length > 0 ? await getActions({ agent_id: { $in: editableAgentIds } }) : []; + editableAgentIds.length > 0 + ? await db.getActions({ agent_id: { $in: editableAgentIds } }) + : []; res.json(actions); } catch (error) { @@ -135,9 +135,9 @@ router.post( const initialPromises = []; // Permissions already validated by middleware - load agent directly - initialPromises.push(getAgent({ id: agent_id })); + initialPromises.push(db.getAgent({ id: agent_id })); if (_action_id) { - initialPromises.push(getActions({ action_id }, true)); + initialPromises.push(db.getActions({ action_id }, true)); } /** @type {[Agent, [Action|undefined]]} */ @@ -184,7 +184,7 @@ router.post( .concat(functions.map((tool) => `${tool.function.name}${actionDelimiter}${encodedDomain}`)); // Force version update since actions are changing - const updatedAgent = await updateAgent( + const updatedAgent = await db.updateAgent( { id: agent_id }, { tools, actions }, { @@ -201,7 +201,7 @@ router.post( } /** @type {[Action]} */ - const updatedAction = await updateAction({ action_id, agent_id }, actionUpdateData); + const updatedAction = await db.updateAction({ action_id, agent_id }, actionUpdateData); const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret']; for (let field of sensitiveFields) { @@ -238,7 +238,7 @@ router.delete( const { agent_id, action_id } = req.params; // Permissions already validated by middleware - load agent directly - const agent = await getAgent({ id: agent_id }); + const agent = await db.getAgent({ id: agent_id }); if (!agent) { return res.status(404).json({ message: 'Agent not found for deleting action' }); } @@ -263,12 +263,12 @@ router.delete( ); // Force version update since actions are being removed - await updateAgent( + await db.updateAgent( { id: agent_id }, { tools: updatedTools, actions: updatedActions }, { updatingUserId: req.user.id, forceVersion: true }, ); - const deleted = await deleteAction({ action_id, agent_id }); + const deleted = await db.deleteAction({ action_id, agent_id }); if (!deleted) { logger.warn('[Agent Action Delete] No matching action document found', { action_id, diff --git a/api/server/routes/agents/chat.js b/api/server/routes/agents/chat.js index 37b83f4f54..0543b0b1aa 100644 --- a/api/server/routes/agents/chat.js +++ b/api/server/routes/agents/chat.js @@ -11,7 +11,7 @@ const { const { initializeClient } = require('~/server/services/Endpoints/agents'); const AgentController = require('~/server/controllers/agents/request'); const addTitle = require('~/server/services/Endpoints/agents/title'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); const router = express.Router(); diff --git a/api/server/routes/agents/index.js b/api/server/routes/agents/index.js index a99fdca592..86966a3f3e 100644 --- a/api/server/routes/agents/index.js +++ b/api/server/routes/agents/index.js @@ -10,8 +10,8 @@ const { messageUserLimiter, } = require('~/server/middleware'); const { saveMessage } = require('~/models'); -const openai = require('./openai'); const responses = require('./responses'); +const openai = require('./openai'); const { v1 } = require('./v1'); const chat = require('./chat'); @@ -263,9 +263,15 @@ router.post('/chat/abort', async (req, res) => { }; try { - await saveMessage(req, responseMessage, { - context: 'api/server/routes/agents/index.js - abort endpoint', - }); + await saveMessage( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, + responseMessage, + { context: 'api/server/routes/agents/index.js - abort endpoint' }, + ); logger.debug(`[AgentStream] Saved partial response for: ${jobStreamId}`); } catch (saveError) { logger.error(`[AgentStream] Failed to save partial response: ${saveError.message}`); diff --git a/api/server/routes/agents/openai.js b/api/server/routes/agents/openai.js index 9a0d9a3564..72e3da6c5a 100644 --- a/api/server/routes/agents/openai.js +++ b/api/server/routes/agents/openai.js @@ -29,26 +29,24 @@ const { GetModelController, } = require('~/server/controllers/agents/openai'); const { getEffectivePermissions } = require('~/server/services/PermissionService'); -const { validateAgentApiKey, findUser } = require('~/models'); const { configMiddleware } = require('~/server/middleware'); -const { getRoleByName } = require('~/models/Role'); -const { getAgent } = require('~/models/Agent'); +const db = require('~/models'); const router = express.Router(); const requireApiKeyAuth = createRequireApiKeyAuth({ - validateAgentApiKey, - findUser, + validateAgentApiKey: db.validateAgentApiKey, + findUser: db.findUser, }); const checkRemoteAgentsFeature = generateCheckAccess({ permissionType: PermissionTypes.REMOTE_AGENTS, permissions: [Permissions.USE], - getRoleByName, + getRoleByName: db.getRoleByName, }); const checkAgentPermission = createCheckRemoteAgentAccess({ - getAgent, + getAgent: db.getAgent, getEffectivePermissions, }); diff --git a/api/server/routes/agents/responses.js b/api/server/routes/agents/responses.js index 431942e921..2c118e0597 100644 --- a/api/server/routes/agents/responses.js +++ b/api/server/routes/agents/responses.js @@ -32,26 +32,24 @@ const { listModels, } = require('~/server/controllers/agents/responses'); const { getEffectivePermissions } = require('~/server/services/PermissionService'); -const { validateAgentApiKey, findUser } = require('~/models'); const { configMiddleware } = require('~/server/middleware'); -const { getRoleByName } = require('~/models/Role'); -const { getAgent } = require('~/models/Agent'); +const db = require('~/models'); const router = express.Router(); const requireApiKeyAuth = createRequireApiKeyAuth({ - validateAgentApiKey, - findUser, + validateAgentApiKey: db.validateAgentApiKey, + findUser: db.findUser, }); const checkRemoteAgentsFeature = generateCheckAccess({ permissionType: PermissionTypes.REMOTE_AGENTS, permissions: [Permissions.USE], - getRoleByName, + getRoleByName: db.getRoleByName, }); const checkAgentPermission = createCheckRemoteAgentAccess({ - getAgent, + getAgent: db.getAgent, getEffectivePermissions, }); diff --git a/api/server/routes/agents/v1.js b/api/server/routes/agents/v1.js index 0c7d23f8ad..c4f90d0bd5 100644 --- a/api/server/routes/agents/v1.js +++ b/api/server/routes/agents/v1.js @@ -3,7 +3,7 @@ const { generateCheckAccess } = require('@librechat/api'); const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider'); const { requireJwtAuth, configMiddleware, canAccessAgentResource } = require('~/server/middleware'); const v1 = require('~/server/controllers/agents/v1'); -const { getRoleByName } = require('~/models/Role'); +const { getRoleByName } = require('~/models'); const actions = require('./actions'); const tools = require('./tools'); diff --git a/api/server/routes/apiKeys.js b/api/server/routes/apiKeys.js index 29dcc326f5..ee11a8b0dd 100644 --- a/api/server/routes/apiKeys.js +++ b/api/server/routes/apiKeys.js @@ -6,9 +6,9 @@ const { createAgentApiKey, deleteAgentApiKey, listAgentApiKeys, + getRoleByName, } = require('~/models'); const { requireJwtAuth } = require('~/server/middleware'); -const { getRoleByName } = require('~/models/Role'); const router = express.Router(); diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js index 75ab879e2b..977d3f92a7 100644 --- a/api/server/routes/assistants/actions.js +++ b/api/server/routes/assistants/actions.js @@ -9,8 +9,7 @@ const { domainParser, } = require('~/server/services/ActionService'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); -const { updateAction, getActions, deleteAction } = require('~/models/Action'); -const { updateAssistantDoc, getAssistant } = require('~/models/Assistant'); +const db = require('~/models'); const router = express.Router(); @@ -56,9 +55,9 @@ router.post('/:assistant_id', async (req, res) => { const { openai } = await getOpenAIClient({ req, res }); - initialPromises.push(getAssistant({ assistant_id })); + initialPromises.push(db.getAssistant({ assistant_id })); initialPromises.push(openai.beta.assistants.retrieve(assistant_id)); - !!_action_id && initialPromises.push(getActions({ action_id }, true)); + !!_action_id && initialPromises.push(db.getActions({ action_id }, true)); /** @type {[AssistantDocument, Assistant, [Action|undefined]]} */ const [assistant_data, assistant, actions_result] = await Promise.all(initialPromises); @@ -121,7 +120,7 @@ router.post('/:assistant_id', async (req, res) => { if (!assistant_data) { assistantUpdateData.user = req.user.id; } - promises.push(updateAssistantDoc({ assistant_id }, assistantUpdateData)); + promises.push(db.updateAssistantDoc({ assistant_id }, assistantUpdateData)); // Only update user field for new actions const actionUpdateData = { metadata, assistant_id }; @@ -129,7 +128,7 @@ router.post('/:assistant_id', async (req, res) => { // For new actions, use the assistant owner's user ID actionUpdateData.user = assistant_user || req.user.id; } - promises.push(updateAction({ action_id, assistant_id }, actionUpdateData)); + promises.push(db.updateAction({ action_id, assistant_id }, actionUpdateData)); /** @type {[AssistantDocument, Action]} */ let [assistantDocument, updatedAction] = await Promise.all(promises); @@ -171,7 +170,7 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => { const { openai } = await getOpenAIClient({ req, res }); const initialPromises = []; - initialPromises.push(getAssistant({ assistant_id })); + initialPromises.push(db.getAssistant({ assistant_id })); initialPromises.push(openai.beta.assistants.retrieve(assistant_id)); /** @type {[AssistantDocument, Assistant]} */ @@ -209,8 +208,8 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => { if (!assistant_data) { assistantUpdateData.user = req.user.id; } - promises.push(updateAssistantDoc({ assistant_id }, assistantUpdateData)); - promises.push(deleteAction({ action_id, assistant_id })); + promises.push(db.updateAssistantDoc({ assistant_id }, assistantUpdateData)); + promises.push(db.deleteAction({ action_id, assistant_id })); const [, deletedAction] = await Promise.all(promises); if (!deletedAction) { diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index d55684f3de..c660e6f99d 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -17,13 +17,14 @@ const { const { verify2FAWithTempToken } = require('~/server/controllers/auth/TwoFactorAuthController'); const { logoutController } = require('~/server/controllers/auth/LogoutController'); const { loginController } = require('~/server/controllers/auth/LoginController'); +const { findBalanceByUser, upsertBalanceFields } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); const middleware = require('~/server/middleware'); -const { Balance } = require('~/db/models'); const setBalanceConfig = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const router = express.Router(); diff --git a/api/server/routes/banner.js b/api/server/routes/banner.js index cf7eafd017..ad949fd2ca 100644 --- a/api/server/routes/banner.js +++ b/api/server/routes/banner.js @@ -1,13 +1,15 @@ const express = require('express'); - -const { getBanner } = require('~/models/Banner'); +const { logger } = require('@librechat/data-schemas'); const optionalJwtAuth = require('~/server/middleware/optionalJwtAuth'); +const { getBanner } = require('~/models'); + const router = express.Router(); router.get('/', optionalJwtAuth, async (req, res) => { try { res.status(200).send(await getBanner(req.user)); } catch (error) { + logger.error('[getBanner] Error getting banner', error); res.status(500).json({ message: 'Error getting banner' }); } }); diff --git a/api/server/routes/categories.js b/api/server/routes/categories.js index da1828b3ce..612bc37860 100644 --- a/api/server/routes/categories.js +++ b/api/server/routes/categories.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); const { requireJwtAuth } = require('~/server/middleware'); -const { getCategories } = require('~/models/Categories'); +const { getCategories } = require('~/models'); router.get('/', requireJwtAuth, async (req, res) => { try { diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 578796170a..1964075ed3 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -10,14 +10,12 @@ const { createForkLimiters, configMiddleware, } = require('~/server/middleware'); -const { getConvosByCursor, deleteConvos, getConvo, saveConvo } = require('~/models/Conversation'); const { forkConversation, duplicateConversation } = require('~/server/utils/import/fork'); const { storage, importFileFilter } = require('~/server/routes/files/multer'); -const { deleteAllSharedLinks, deleteConvoSharedLink } = require('~/models'); const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); const { importConversations } = require('~/server/utils/import'); -const { deleteToolCalls } = require('~/models/ToolCall'); const getLogStores = require('~/cache/getLogStores'); +const db = require('~/models'); const assistantClients = { [EModelEndpoint.azureAssistants]: require('~/server/services/Endpoints/azureAssistants'), @@ -41,7 +39,7 @@ router.get('/', async (req, res) => { } try { - const result = await getConvosByCursor(req.user.id, { + const result = await db.getConvosByCursor(req.user.id, { cursor, limit, isArchived, @@ -59,7 +57,7 @@ router.get('/', async (req, res) => { router.get('/:conversationId', async (req, res) => { const { conversationId } = req.params; - const convo = await getConvo(req.user.id, conversationId); + const convo = await db.getConvo(req.user.id, conversationId); if (convo) { res.status(200).json(convo); @@ -128,10 +126,10 @@ router.delete('/', async (req, res) => { } try { - const dbResponse = await deleteConvos(req.user.id, filter); + const dbResponse = await db.deleteConvos(req.user.id, filter); if (filter.conversationId) { - await deleteToolCalls(req.user.id, filter.conversationId); - await deleteConvoSharedLink(req.user.id, filter.conversationId); + await db.deleteToolCalls(req.user.id, filter.conversationId); + await db.deleteConvoSharedLink(req.user.id, filter.conversationId); } res.status(201).json(dbResponse); } catch (error) { @@ -142,9 +140,9 @@ router.delete('/', async (req, res) => { router.delete('/all', async (req, res) => { try { - const dbResponse = await deleteConvos(req.user.id, {}); - await deleteToolCalls(req.user.id); - await deleteAllSharedLinks(req.user.id); + const dbResponse = await db.deleteConvos(req.user.id, {}); + await db.deleteToolCalls(req.user.id); + await db.deleteAllSharedLinks(req.user.id); res.status(201).json(dbResponse); } catch (error) { logger.error('Error clearing conversations', error); @@ -171,8 +169,12 @@ router.post('/archive', validateConvoAccess, async (req, res) => { } try { - const dbResponse = await saveConvo( - req, + const dbResponse = await db.saveConvo( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { conversationId, isArchived }, { context: `POST /api/convos/archive ${conversationId}` }, ); @@ -211,8 +213,12 @@ router.post('/update', validateConvoAccess, async (req, res) => { const sanitizedTitle = title.trim().slice(0, MAX_CONVO_TITLE_LENGTH); try { - const dbResponse = await saveConvo( - req, + const dbResponse = await db.saveConvo( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { conversationId, title: sanitizedTitle }, { context: `POST /api/convos/update ${conversationId}` }, ); diff --git a/api/server/routes/files/files.agents.test.js b/api/server/routes/files/files.agents.test.js index 7c21e95234..203c1210fd 100644 --- a/api/server/routes/files/files.agents.test.js +++ b/api/server/routes/files/files.agents.test.js @@ -10,8 +10,7 @@ const { ResourceType, PrincipalType, } = require('librechat-data-provider'); -const { createAgent } = require('~/models/Agent'); -const { createFile } = require('~/models'); +const { createAgent, createFile } = require('~/models'); // Only mock the external dependencies that we don't want to test jest.mock('~/server/services/Files/process', () => ({ diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index fdb7768c3b..a51c00f26e 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -27,25 +27,23 @@ const { checkPermission } = require('~/server/services/PermissionService'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud'); const { hasAccessToFilesViaAgent } = require('~/server/services/Files'); -const { getFiles, batchUpdateFiles } = require('~/models'); const { cleanFileName } = require('~/server/utils/files'); -const { getAssistant } = require('~/models/Assistant'); -const { getAgent } = require('~/models/Agent'); const { getLogStores } = require('~/cache'); const { Readable } = require('stream'); +const db = require('~/models'); const router = express.Router(); router.get('/', async (req, res) => { try { const appConfig = req.config; - const files = await getFiles({ user: req.user.id }); + const files = await db.getFiles({ user: req.user.id }); if (appConfig.fileStrategy === FileSources.s3) { try { const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL); const alreadyChecked = await cache.get(req.user.id); if (!alreadyChecked) { - await refreshS3FileUrls(files, batchUpdateFiles); + await refreshS3FileUrls(files, db.batchUpdateFiles); await cache.set(req.user.id, true, Time.THIRTY_MINUTES); } } catch (error) { @@ -74,7 +72,7 @@ router.get('/agent/:agent_id', async (req, res) => { return res.status(400).json({ error: 'Agent ID is required' }); } - const agent = await getAgent({ id: agent_id }); + const agent = await db.getAgent({ id: agent_id }); if (!agent) { return res.status(200).json([]); } @@ -106,7 +104,7 @@ router.get('/agent/:agent_id', async (req, res) => { return res.status(200).json([]); } - const files = await getFiles({ file_id: { $in: agentFileIds } }, null, { text: 0 }); + const files = await db.getFiles({ file_id: { $in: agentFileIds } }, null, { text: 0 }); res.status(200).json(files); } catch (error) { @@ -151,7 +149,7 @@ router.delete('/', async (req, res) => { } const fileIds = files.map((file) => file.file_id); - const dbFiles = await getFiles({ file_id: { $in: fileIds } }); + const dbFiles = await db.getFiles({ file_id: { $in: fileIds } }); const ownedFiles = []; const nonOwnedFiles = []; @@ -209,7 +207,7 @@ router.delete('/', async (req, res) => { /* Handle agent unlinking even if no valid files to delete */ if (req.body.agent_id && req.body.tool_resource && dbFiles.length === 0) { - const agent = await getAgent({ + const agent = await db.getAgent({ id: req.body.agent_id, }); @@ -223,7 +221,7 @@ router.delete('/', async (req, res) => { /* Handle assistant unlinking even if no valid files to delete */ if (req.body.assistant_id && req.body.tool_resource && dbFiles.length === 0) { - const assistant = await getAssistant({ + const assistant = await db.getAssistant({ id: req.body.assistant_id, }); @@ -385,7 +383,7 @@ router.post('/', async (req, res) => { req, res, metadata, - getAgent, + getAgent: db.getAgent, checkPermission, }); if (denied) { diff --git a/api/server/routes/files/files.test.js b/api/server/routes/files/files.test.js index 1d548b44be..457ebabe92 100644 --- a/api/server/routes/files/files.test.js +++ b/api/server/routes/files/files.test.js @@ -10,8 +10,7 @@ const { AccessRoleIds, PrincipalType, } = require('librechat-data-provider'); -const { createAgent } = require('~/models/Agent'); -const { createFile } = require('~/models'); +const { createAgent, createFile } = require('~/models'); // Only mock the external dependencies that we don't want to test jest.mock('~/server/services/Files/process', () => ({ diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 57a99d199a..d6d7ed5ea0 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -38,13 +38,11 @@ const { } = require('~/config'); const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); const { requireJwtAuth, canAccessMCPServerResource } = require('~/server/middleware'); -const { findToken, updateToken, createToken, deleteTokens } = require('~/models'); const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { updateMCPServerTools } = require('~/server/services/Config/mcp'); const { reinitMCPServer } = require('~/server/services/Tools/mcp'); -const { findPluginAuthsByKeys } = require('~/models'); -const { getRoleByName } = require('~/models/Role'); const { getLogStores } = require('~/cache'); +const db = require('~/models'); const router = Router(); @@ -53,13 +51,13 @@ const OAUTH_CSRF_COOKIE_PATH = '/api/mcp'; const checkMCPUsePermissions = generateCheckAccess({ permissionType: PermissionTypes.MCP_SERVERS, permissions: [Permissions.USE], - getRoleByName, + getRoleByName: db.getRoleByName, }); const checkMCPCreate = generateCheckAccess({ permissionType: PermissionTypes.MCP_SERVERS, permissions: [Permissions.USE, Permissions.CREATE], - getRoleByName, + getRoleByName: db.getRoleByName, }); /** @@ -246,9 +244,9 @@ router.get('/:serverName/oauth/callback', async (req, res) => { userId: flowState.userId, serverName, tokens, - createToken, - updateToken, - findToken, + createToken: db.createToken, + updateToken: db.updateToken, + findToken: db.findToken, clientInfo: flowState.clientInfo, metadata: flowState.metadata, }); @@ -286,10 +284,10 @@ router.get('/:serverName/oauth/callback', async (req, res) => { serverName, flowManager, tokenMethods: { - findToken, - updateToken, - createToken, - deleteTokens, + findToken: db.findToken, + updateToken: db.updateToken, + createToken: db.createToken, + deleteTokens: db.deleteTokens, }, }); @@ -517,7 +515,7 @@ router.post( userMCPAuthMap = await getUserMCPAuthMap({ userId: user.id, servers: [serverName], - findPluginAuthsByKeys, + findPluginAuthsByKeys: db.findPluginAuthsByKeys, }); } diff --git a/api/server/routes/memories.js b/api/server/routes/memories.js index 58955d8ec4..e71e94f457 100644 --- a/api/server/routes/memories.js +++ b/api/server/routes/memories.js @@ -4,12 +4,12 @@ const { PermissionTypes, Permissions } = require('librechat-data-provider'); const { getAllUserMemories, toggleUserMemories, + getRoleByName, createMemory, deleteMemory, setMemory, } = require('~/models'); const { requireJwtAuth, configMiddleware } = require('~/server/middleware'); -const { getRoleByName } = require('~/models/Role'); const router = express.Router(); diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index 03286bc7f1..21b2b23fea 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -3,18 +3,9 @@ const { v4: uuidv4 } = require('uuid'); const { logger } = require('@librechat/data-schemas'); const { ContentTypes } = require('librechat-data-provider'); const { unescapeLaTeX, countTokens } = require('@librechat/api'); -const { - saveConvo, - getMessage, - saveMessage, - getMessages, - updateMessage, - deleteMessages, -} = require('~/models'); const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update'); const { requireJwtAuth, validateMessageReq } = require('~/server/middleware'); -const { getConvosQueried } = require('~/models/Conversation'); -const { Message } = require('~/db/models'); +const db = require('~/models'); const router = express.Router(); router.use(requireJwtAuth); @@ -40,34 +31,19 @@ router.get('/', async (req, res) => { const sortOrder = sortDirection === 'asc' ? 1 : -1; if (conversationId && messageId) { - const message = await Message.findOne({ - conversationId, - messageId, - user: user, - }).lean(); - response = { messages: message ? [message] : [], nextCursor: null }; + const messages = await db.getMessages({ conversationId, messageId, user }); + response = { messages: messages?.length ? [messages[0]] : [], nextCursor: null }; } else if (conversationId) { - const filter = { conversationId, user: user }; - if (cursor) { - filter[sortField] = sortOrder === 1 ? { $gt: cursor } : { $lt: cursor }; - } - const messages = await Message.find(filter) - .sort({ [sortField]: sortOrder }) - .limit(pageSize + 1) - .lean(); - let nextCursor = null; - if (messages.length > pageSize) { - messages.pop(); // Remove extra item used to detect next page - // Create cursor from the last RETURNED item (not the popped one) - nextCursor = messages[messages.length - 1][sortField]; - } - response = { messages, nextCursor }; + response = await db.getMessagesByCursor( + { conversationId, user }, + { sortField, sortOrder, limit: pageSize, cursor }, + ); } else if (search) { - const searchResults = await Message.meiliSearch(search, { filter: `user = "${user}"` }, true); + const searchResults = await db.searchMessages(search, { filter: `user = "${user}"` }, true); const messages = searchResults.hits || []; - const result = await getConvosQueried(req.user.id, messages, cursor); + const result = await db.getConvosQueried(req.user.id, messages, cursor); const messageIds = []; const cleanedMessages = []; @@ -79,7 +55,7 @@ router.get('/', async (req, res) => { } } - const dbMessages = await getMessages({ + const dbMessages = await db.getMessages({ user, messageId: { $in: messageIds }, }); @@ -136,7 +112,7 @@ router.post('/branch', async (req, res) => { return res.status(400).json({ error: 'messageId and agentId are required' }); } - const sourceMessage = await getMessage({ user: userId, messageId }); + const sourceMessage = await db.getMessage({ user: userId, messageId }); if (!sourceMessage) { return res.status(404).json({ error: 'Source message not found' }); } @@ -187,9 +163,15 @@ router.post('/branch', async (req, res) => { user: userId, }; - const savedMessage = await saveMessage(req, newMessage, { - context: 'POST /api/messages/branch', - }); + const savedMessage = await db.saveMessage( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, + newMessage, + { context: 'POST /api/messages/branch' }, + ); if (!savedMessage) { return res.status(500).json({ error: 'Failed to save branch message' }); @@ -211,7 +193,7 @@ router.post('/artifact/:messageId', async (req, res) => { return res.status(400).json({ error: 'Invalid request parameters' }); } - const message = await getMessage({ user: req.user.id, messageId }); + const message = await db.getMessage({ user: req.user.id, messageId }); if (!message) { return res.status(404).json({ error: 'Message not found' }); } @@ -256,8 +238,12 @@ router.post('/artifact/:messageId', async (req, res) => { return res.status(400).json({ error: 'Original content not found in target artifact' }); } - const savedMessage = await saveMessage( - req, + const savedMessage = await db.saveMessage( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { messageId, conversationId: message.conversationId, @@ -283,7 +269,7 @@ router.post('/artifact/:messageId', async (req, res) => { router.get('/:conversationId', validateMessageReq, async (req, res) => { try { const { conversationId } = req.params; - const messages = await getMessages({ conversationId }, '-_id -__v -user'); + const messages = await db.getMessages({ conversationId }, '-_id -__v -user'); res.status(200).json(messages); } catch (error) { logger.error('Error fetching messages:', error); @@ -294,15 +280,20 @@ router.get('/:conversationId', validateMessageReq, async (req, res) => { router.post('/:conversationId', validateMessageReq, async (req, res) => { try { const message = req.body; - const savedMessage = await saveMessage( - req, + const reqCtx = { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }; + const savedMessage = await db.saveMessage( + reqCtx, { ...message, user: req.user.id }, { context: 'POST /api/messages/:conversationId' }, ); if (!savedMessage) { return res.status(400).json({ error: 'Message not saved' }); } - await saveConvo(req, savedMessage, { context: 'POST /api/messages/:conversationId' }); + await db.saveConvo(reqCtx, savedMessage, { context: 'POST /api/messages/:conversationId' }); res.status(201).json(savedMessage); } catch (error) { logger.error('Error saving message:', error); @@ -313,7 +304,7 @@ router.post('/:conversationId', validateMessageReq, async (req, res) => { router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { const { conversationId, messageId } = req.params; - const message = await getMessages({ conversationId, messageId }, '-_id -__v -user'); + const message = await db.getMessages({ conversationId, messageId }, '-_id -__v -user'); if (!message) { return res.status(404).json({ error: 'Message not found' }); } @@ -331,7 +322,7 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) = if (index === undefined) { const tokenCount = await countTokens(text, model); - const result = await updateMessage(req, { messageId, text, tokenCount }); + const result = await db.updateMessage(req?.user?.id, { messageId, text, tokenCount }); return res.status(200).json(result); } @@ -339,7 +330,9 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) = return res.status(400).json({ error: 'Invalid index' }); } - const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0]; + const message = ( + await db.getMessages({ conversationId, messageId }, 'content tokenCount') + )?.[0]; if (!message) { return res.status(404).json({ error: 'Message not found' }); } @@ -369,7 +362,11 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) = tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount; } - const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount }); + const result = await db.updateMessage(req?.user?.id, { + messageId, + content: updatedContent, + tokenCount, + }); return res.status(200).json(result); } catch (error) { logger.error('Error updating message:', error); @@ -382,8 +379,8 @@ router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (re const { conversationId, messageId } = req.params; const { feedback } = req.body; - const updatedMessage = await updateMessage( - req, + const updatedMessage = await db.updateMessage( + req?.user?.id, { messageId, feedback: feedback || null, @@ -405,7 +402,7 @@ router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (re router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { const { conversationId, messageId } = req.params; - await deleteMessages({ messageId, conversationId, user: req.user.id }); + await db.deleteMessages({ messageId, conversationId, user: req.user.id }); res.status(204).send(); } catch (error) { logger.error('Error deleting message:', error); diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js index f4bb5b6026..5302158031 100644 --- a/api/server/routes/oauth.js +++ b/api/server/routes/oauth.js @@ -7,12 +7,13 @@ const { ErrorTypes } = require('librechat-data-provider'); const { createSetBalanceConfig } = require('@librechat/api'); const { checkDomainAllowed, loginLimiter, logHeaders } = require('~/server/middleware'); const { createOAuthHandler } = require('~/server/controllers/auth/oauth'); +const { findBalanceByUser, upsertBalanceFields } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); -const { Balance } = require('~/db/models'); const setBalanceConfig = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const router = express.Router(); diff --git a/api/server/routes/prompts.js b/api/server/routes/prompts.js index a0fe65ffd1..d437273df2 100644 --- a/api/server/routes/prompts.js +++ b/api/server/routes/prompts.js @@ -25,11 +25,12 @@ const { deletePromptGroup, createPromptGroup, getPromptGroup, + getRoleByName, deletePrompt, getPrompts, savePrompt, getPrompt, -} = require('~/models/Prompt'); +} = require('~/models'); const { canAccessPromptGroupResource, canAccessPromptViaGroup, @@ -41,7 +42,6 @@ const { findAccessibleResources, grantPermission, } = require('~/server/services/PermissionService'); -const { getRoleByName } = require('~/models/Role'); const router = express.Router(); diff --git a/api/server/routes/prompts.test.js b/api/server/routes/prompts.test.js index caeb90ddfb..80c973147f 100644 --- a/api/server/routes/prompts.test.js +++ b/api/server/routes/prompts.test.js @@ -16,9 +16,22 @@ jest.mock('~/server/services/Config', () => ({ getCachedTools: jest.fn().mockResolvedValue({}), })); -jest.mock('~/models/Role', () => ({ - getRoleByName: jest.fn(), -})); +jest.mock('~/models', () => { + const mongoose = require('mongoose'); + const { createMethods } = require('@librechat/data-schemas'); + const methods = createMethods(mongoose, { + removeAllPermissions: async ({ resourceType, resourceId }) => { + const AclEntry = mongoose.models.AclEntry; + if (AclEntry) { + await AclEntry.deleteMany({ resourceType, resourceId }); + } + }, + }); + return { + ...methods, + getRoleByName: jest.fn(), + }; +}); jest.mock('~/server/middleware', () => ({ requireJwtAuth: (req, res, next) => next(), @@ -153,7 +166,7 @@ async function setupTestData() { }; // Mock getRoleByName - const { getRoleByName } = require('~/models/Role'); + const { getRoleByName } = require('~/models'); getRoleByName.mockImplementation((roleName) => { switch (roleName) { case SystemRoles.USER: diff --git a/api/server/routes/roles.js b/api/server/routes/roles.js index 12e18c7624..4c0f044f76 100644 --- a/api/server/routes/roles.js +++ b/api/server/routes/roles.js @@ -12,7 +12,7 @@ const { remoteAgentsPermissionsSchema, } = require('librechat-data-provider'); const { checkAdmin, requireJwtAuth } = require('~/server/middleware'); -const { updateRoleByName, getRoleByName } = require('~/models/Role'); +const { updateRoleByName, getRoleByName } = require('~/models'); const router = express.Router(); router.use(requireJwtAuth); diff --git a/api/server/routes/tags.js b/api/server/routes/tags.js index 0a4ee5084c..a1fa1f77bb 100644 --- a/api/server/routes/tags.js +++ b/api/server/routes/tags.js @@ -8,9 +8,9 @@ const { createConversationTag, deleteConversationTag, getConversationTags, -} = require('~/models/ConversationTag'); + getRoleByName, +} = require('~/models'); const { requireJwtAuth } = require('~/server/middleware'); -const { getRoleByName } = require('~/models/Role'); const router = express.Router(); diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index bde052bba4..c8ed7bebc4 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -20,9 +20,14 @@ const { isImageVisionTool, actionDomainSeparator, } = require('librechat-data-provider'); -const { findToken, updateToken, createToken } = require('~/models'); -const { getActions, deleteActions } = require('~/models/Action'); -const { deleteAssistant } = require('~/models/Assistant'); +const { + findToken, + updateToken, + createToken, + getActions, + deleteActions, + deleteAssistant, +} = require('~/models'); const { getFlowStateManager } = require('~/config'); const { getLogStores } = require('~/cache'); diff --git a/api/server/services/Endpoints/agents/addedConvo.js b/api/server/services/Endpoints/agents/addedConvo.js index 11b87e450e..7561053f8f 100644 --- a/api/server/services/Endpoints/agents/addedConvo.js +++ b/api/server/services/Endpoints/agents/addedConvo.js @@ -1,13 +1,16 @@ const { logger } = require('@librechat/data-schemas'); -const { initializeAgent, validateAgentModel } = require('@librechat/api'); -const { loadAddedAgent, setGetAgent, ADDED_AGENT_ID } = require('~/models/loadAddedAgent'); +const { + ADDED_AGENT_ID, + initializeAgent, + validateAgentModel, + loadAddedAgent: loadAddedAgentFn, +} = require('@librechat/api'); const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); -const { getConvoFiles } = require('~/models/Conversation'); -const { getAgent } = require('~/models/Agent'); +const { getMCPServerTools } = require('~/server/services/Config'); const db = require('~/models'); -// Initialize the getAgent dependency -setGetAgent(getAgent); +const loadAddedAgent = (params) => + loadAddedAgentFn(params, { getAgent: db.getAgent, getMCPServerTools }); /** * Process addedConvo for parallel agent execution. @@ -100,10 +103,10 @@ const processAddedConvo = async ({ allowedProviders, }, { - getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, getMessages: db.getMessages, + getConvoFiles: db.getConvoFiles, updateFilesUsage: db.updateFilesUsage, getUserCodeFiles: db.getUserCodeFiles, getUserKeyValues: db.getUserKeyValues, diff --git a/api/server/services/Endpoints/agents/build.js b/api/server/services/Endpoints/agents/build.js index a95640e528..19ae3ab7e8 100644 --- a/api/server/services/Endpoints/agents/build.js +++ b/api/server/services/Endpoints/agents/build.js @@ -1,6 +1,10 @@ const { logger } = require('@librechat/data-schemas'); +const { loadAgent: loadAgentFn } = require('@librechat/api'); const { isAgentsEndpoint, removeNullishValues, Constants } = require('librechat-data-provider'); -const { loadAgent } = require('~/models/Agent'); +const { getMCPServerTools } = require('~/server/services/Config'); +const db = require('~/models'); + +const loadAgent = (params) => loadAgentFn(params, { getAgent: db.getAgent, getMCPServerTools }); const buildOptions = (req, endpoint, parsedBody, endpointType) => { const { spec, iconURL, agent_id, ...model_parameters } = parsedBody; diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 08f631c3d2..28282e68ea 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -26,9 +26,7 @@ const { filterFilesByAgentAccess } = require('~/server/services/Files/permission const { getModelsConfig } = require('~/server/controllers/ModelController'); const { checkPermission } = require('~/server/services/PermissionService'); const AgentClient = require('~/server/controllers/agents/client'); -const { getConvoFiles } = require('~/models/Conversation'); const { processAddedConvo } = require('./addedConvo'); -const { getAgent } = require('~/models/Agent'); const { logViolation } = require('~/cache'); const db = require('~/models'); @@ -196,10 +194,10 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { isInitialAgent: true, }, { - getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, getMessages: db.getMessages, + getConvoFiles: db.getConvoFiles, updateFilesUsage: db.updateFilesUsage, getUserKeyValues: db.getUserKeyValues, getUserCodeFiles: db.getUserCodeFiles, @@ -227,7 +225,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { const skippedAgentIds = new Set(); async function processAgent(agentId) { - const agent = await getAgent({ id: agentId }); + const agent = await db.getAgent({ id: agentId }); if (!agent) { logger.warn( `[processAgent] Handoff agent ${agentId} not found, skipping (orphaned reference)`, @@ -277,10 +275,10 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { allowedProviders, }, { - getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, getMessages: db.getMessages, + getConvoFiles: db.getConvoFiles, updateFilesUsage: db.updateFilesUsage, getUserKeyValues: db.getUserKeyValues, getUserCodeFiles: db.getUserCodeFiles, diff --git a/api/server/services/Endpoints/agents/title.js b/api/server/services/Endpoints/agents/title.js index e31cdeea11..b7e1a54e06 100644 --- a/api/server/services/Endpoints/agents/title.js +++ b/api/server/services/Endpoints/agents/title.js @@ -66,7 +66,11 @@ const addTitle = async (req, { text, response, client }) => { await titleCache.set(key, title, 120000); await saveConvo( - req, + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { conversationId: response.conversationId, title, diff --git a/api/server/services/Endpoints/assistants/build.js b/api/server/services/Endpoints/assistants/build.js index 00a2abf606..85f7090211 100644 --- a/api/server/services/Endpoints/assistants/build.js +++ b/api/server/services/Endpoints/assistants/build.js @@ -1,6 +1,6 @@ const { removeNullishValues } = require('librechat-data-provider'); const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts'); -const { getAssistant } = require('~/models/Assistant'); +const { getAssistant } = require('~/models'); const buildOptions = async (endpoint, parsedBody) => { const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } = diff --git a/api/server/services/Endpoints/assistants/title.js b/api/server/services/Endpoints/assistants/title.js index 1fae68cf54..b31289eb60 100644 --- a/api/server/services/Endpoints/assistants/title.js +++ b/api/server/services/Endpoints/assistants/title.js @@ -1,9 +1,9 @@ const { isEnabled, sanitizeTitle } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys } = require('librechat-data-provider'); -const { saveConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); const initializeClient = require('./initalize'); +const { saveConvo } = require('~/models'); /** * Generates a conversation title using OpenAI SDK @@ -63,8 +63,13 @@ const addTitle = async (req, { text, responseText, conversationId }) => { const title = await generateTitle({ openai, text, responseText }); await titleCache.set(key, title, 120000); + const reqCtx = { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }; await saveConvo( - req, + reqCtx, { conversationId, title, @@ -76,7 +81,11 @@ const addTitle = async (req, { text, responseText, conversationId }) => { const fallbackTitle = text.length > 40 ? text.substring(0, 37) + '...' : text; await titleCache.set(key, fallbackTitle, 120000); await saveConvo( - req, + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { conversationId, title: fallbackTitle, diff --git a/api/server/services/Endpoints/azureAssistants/build.js b/api/server/services/Endpoints/azureAssistants/build.js index 53b1dbeb68..315447ed7f 100644 --- a/api/server/services/Endpoints/azureAssistants/build.js +++ b/api/server/services/Endpoints/azureAssistants/build.js @@ -1,6 +1,6 @@ const { removeNullishValues } = require('librechat-data-provider'); const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts'); -const { getAssistant } = require('~/models/Assistant'); +const { getAssistant } = require('~/models'); const buildOptions = async (endpoint, parsedBody) => { const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } = diff --git a/api/server/services/Files/Audio/streamAudio.js b/api/server/services/Files/Audio/streamAudio.js index a1d7c7a649..c28a96edff 100644 --- a/api/server/services/Files/Audio/streamAudio.js +++ b/api/server/services/Files/Audio/streamAudio.js @@ -5,8 +5,8 @@ const { parseTextParts, findLastSeparatorIndex, } = require('librechat-data-provider'); -const { getMessage } = require('~/models/Message'); const { getLogStores } = require('~/cache'); +const { getMessage } = require('~/models'); /** * @param {string[]} voiceIds - Array of voice IDs diff --git a/api/server/services/Files/Audio/streamAudio.spec.js b/api/server/services/Files/Audio/streamAudio.spec.js index e76c0849c7..977d8730aa 100644 --- a/api/server/services/Files/Audio/streamAudio.spec.js +++ b/api/server/services/Files/Audio/streamAudio.spec.js @@ -3,7 +3,7 @@ const { createChunkProcessor, splitTextIntoChunks } = require('./streamAudio'); jest.mock('keyv'); const globalCache = {}; -jest.mock('~/models/Message', () => { +jest.mock('~/models', () => { return { getMessage: jest.fn().mockImplementation((messageId) => { return globalCache[messageId] || null; diff --git a/api/server/services/Files/Citations/index.js b/api/server/services/Files/Citations/index.js index 7cb2ee6de0..008e21d7c4 100644 --- a/api/server/services/Files/Citations/index.js +++ b/api/server/services/Files/Citations/index.js @@ -8,8 +8,7 @@ const { EModelEndpoint, PermissionTypes, } = require('librechat-data-provider'); -const { getRoleByName } = require('~/models/Role'); -const { Files } = require('~/models'); +const { getRoleByName, getFiles } = require('~/models'); /** * Process file search results from tool calls @@ -127,7 +126,7 @@ async function enhanceSourcesWithMetadata(sources, appConfig) { let fileMetadataMap = {}; try { - const files = await Files.find({ file_id: { $in: fileIds } }); + const files = await getFiles({ file_id: { $in: fileIds } }); fileMetadataMap = files.reduce((map, file) => { map[file.file_id] = file; return map; diff --git a/api/server/services/Files/permissions.js b/api/server/services/Files/permissions.js index b9a5d6656f..ffa8e74799 100644 --- a/api/server/services/Files/permissions.js +++ b/api/server/services/Files/permissions.js @@ -1,7 +1,7 @@ const { logger } = require('@librechat/data-schemas'); const { PermissionBits, ResourceType, isEphemeralAgentId } = require('librechat-data-provider'); const { checkPermission } = require('~/server/services/PermissionService'); -const { getAgent } = require('~/models/Agent'); +const { getAgent } = require('~/models'); /** * @param {Object} agent - The agent document (lean) diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index d01128927a..f7d7731975 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -27,16 +27,15 @@ const { resizeImageBuffer, } = require('~/server/services/Files/images'); const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2'); -const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Agent'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); -const { createFile, updateFileUsage, deleteFiles } = require('~/models'); const { getFileStrategy } = require('~/server/utils/getFileStrategy'); const { checkCapability } = require('~/server/services/Config'); const { LB_QueueAsyncCall } = require('~/server/utils/queue'); const { getStrategyFunctions } = require('./strategies'); const { determineFileType } = require('~/server/utils'); const { STTService } = require('./Audio/STTService'); +const db = require('~/models'); /** * Creates a modular file upload wrapper that ensures filename sanitization @@ -211,7 +210,7 @@ const processDeleteRequest = async ({ req, files }) => { if (agentFiles.length > 0) { promises.push( - removeAgentResourceFiles({ + db.removeAgentResourceFiles({ agent_id: req.body.agent_id, files: agentFiles, }), @@ -219,7 +218,7 @@ const processDeleteRequest = async ({ req, files }) => { } await Promise.allSettled(promises); - await deleteFiles(resolvedFileIds); + await db.deleteFiles(resolvedFileIds); }; /** @@ -251,7 +250,7 @@ const processFileURL = async ({ fileStrategy, userId, URL, fileName, basePath, c dimensions = {}, } = (await saveURL({ userId, URL, fileName, basePath })) || {}; const filepath = await getFileURL({ fileName: `${userId}/${fileName}`, basePath }); - return await createFile( + return await db.createFile( { user: userId, file_id: v4(), @@ -297,7 +296,7 @@ const processImageFile = async ({ req, res, metadata, returnFile = false }) => { endpoint, }); - const result = await createFile( + const result = await db.createFile( { user: req.user.id, file_id, @@ -349,7 +348,7 @@ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true }) } const fileName = `${file_id}-${filename}`; const filepath = await saveBuffer({ userId: req.user.id, fileName, buffer }); - return await createFile( + return await db.createFile( { user: req.user.id, file_id, @@ -435,7 +434,7 @@ const processFileUpload = async ({ req, res, metadata }) => { filepath = result.filepath; } - const result = await createFile( + const result = await db.createFile( { user: req.user.id, file_id: id ?? file_id, @@ -545,14 +544,14 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { }); if (!messageAttachment && tool_resource) { - await addAgentResourceFile({ - req, + await db.addAgentResourceFile({ file_id, agent_id, tool_resource, + updatingUserId: req?.user?.id, }); } - const result = await createFile(fileInfo, true); + const result = await db.createFile(fileInfo, true); return res .status(200) .json({ message: 'Agent file uploaded and processed successfully', ...result }); @@ -685,11 +684,11 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { let filepath = _filepath; if (!messageAttachment && tool_resource) { - await addAgentResourceFile({ - req, + await db.addAgentResourceFile({ file_id, agent_id, tool_resource, + updatingUserId: req?.user?.id, }); } @@ -720,7 +719,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { width, }); - const result = await createFile(fileInfo, true); + const result = await db.createFile(fileInfo, true); res.status(200).json({ message: 'Agent file uploaded and processed successfully', ...result }); }; @@ -766,10 +765,10 @@ const processOpenAIFile = async ({ }; if (saveFile) { - await createFile(file, true); + await db.createFile(file, true); } else if (updateUsage) { try { - await updateFileUsage({ file_id }); + await db.updateFileUsage({ file_id }); } catch (error) { logger.error('Error updating file usage', error); } @@ -807,7 +806,7 @@ const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileEx file_id, filename, }; - createFile(file, true); + db.createFile(file, true); return file; }; @@ -951,7 +950,7 @@ async function saveBase64Image( fileName: filename, buffer: image.buffer, }); - return await createFile( + return await db.createFile( { type, source, diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index c82ee02599..f7b6be612f 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -20,16 +20,22 @@ const { getEffectivePermissionsForResources: getEffectivePermissionsForResourcesACL, grantPermission: grantPermissionACL, findEntriesByPrincipalsAndResource, + findRolesByResourceType, + findPublicResourceIds, + bulkWriteAclEntries, findGroupByExternalId, findRoleByIdentifier, + deleteAclEntries, getUserPrincipals, + findGroupByQuery, + updateGroupById, + bulkUpdateGroups, hasPermission, createGroup, createUser, updateUser, findUser, } = require('~/models'); -const { AclEntry, AccessRole, Group } = require('~/db/models'); /** @type {boolean|null} */ let transactionSupportCache = null; @@ -280,17 +286,9 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio validateResourceType(resourceType); - // Find all public ACL entries where the public principal has at least the required permission bits - const entries = await AclEntry.find({ - principalType: PrincipalType.PUBLIC, - resourceType, - permBits: { $bitsAllSet: requiredPermissions }, - }).distinct('resourceId'); - - return entries; + return await findPublicResourceIds(resourceType, requiredPermissions); } catch (error) { logger.error(`[PermissionService.findPubliclyAccessibleResources] Error: ${error.message}`); - // Re-throw validation errors if (error.message.includes('requiredPermissions must be')) { throw error; } @@ -307,7 +305,7 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio const getAvailableRoles = async ({ resourceType }) => { validateResourceType(resourceType); - return await AccessRole.find({ resourceType }).lean(); + return await findRolesByResourceType(resourceType); }; /** @@ -428,7 +426,7 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null let existingGroup = await findGroupByExternalId(principal.idOnTheSource, 'entra'); if (!existingGroup && principal.email) { - existingGroup = await Group.findOne({ email: principal.email.toLowerCase() }).lean(); + existingGroup = await findGroupByQuery({ email: principal.email.toLowerCase() }); } if (existingGroup) { @@ -457,7 +455,7 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null } if (needsUpdate) { - await Group.findByIdAndUpdate(existingGroup._id, { $set: updateData }, { new: true }); + await updateGroupById(existingGroup._id, updateData); } return existingGroup._id.toString(); @@ -525,7 +523,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null) const sessionOptions = session ? { session } : {}; - await Group.updateMany( + await bulkUpdateGroups( { idOnTheSource: { $in: allGroupIds }, source: 'entra', @@ -535,7 +533,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null) sessionOptions, ); - await Group.updateMany( + await bulkUpdateGroups( { source: 'entra', memberIds: user.idOnTheSource, @@ -633,7 +631,7 @@ const bulkUpdateResourcePermissions = async ({ const sessionOptions = localSession ? { session: localSession } : {}; - const roles = await AccessRole.find({ resourceType }).lean(); + const roles = await findRolesByResourceType(resourceType); const rolesMap = new Map(); roles.forEach((role) => { rolesMap.set(role.accessRoleId, role); @@ -737,7 +735,7 @@ const bulkUpdateResourcePermissions = async ({ } if (bulkWrites.length > 0) { - await AclEntry.bulkWrite(bulkWrites, sessionOptions); + await bulkWriteAclEntries(bulkWrites, sessionOptions); } const deleteQueries = []; @@ -778,12 +776,7 @@ const bulkUpdateResourcePermissions = async ({ } if (deleteQueries.length > 0) { - await AclEntry.deleteMany( - { - $or: deleteQueries, - }, - sessionOptions, - ); + await deleteAclEntries({ $or: deleteQueries }, sessionOptions); } if (shouldEndSession && supportsTransactions) { @@ -870,7 +863,7 @@ const removeAllPermissions = async ({ resourceType, resourceId }) => { throw new Error(`Invalid resource ID: ${resourceId}`); } - const result = await AclEntry.deleteMany({ + const result = await deleteAclEntries({ resourceType, resourceId, }); diff --git a/api/server/services/Threads/manage.js b/api/server/services/Threads/manage.js index 627dba1a35..27520f38a5 100644 --- a/api/server/services/Threads/manage.js +++ b/api/server/services/Threads/manage.js @@ -1,16 +1,15 @@ const path = require('path'); const { v4 } = require('uuid'); -const { countTokens, escapeRegExp } = require('@librechat/api'); +const { countTokens } = require('@librechat/api'); +const { escapeRegExp } = require('@librechat/data-schemas'); const { Constants, ContentTypes, AnnotationTypes, defaultOrderQuery, } = require('librechat-data-provider'); +const { recordMessage, getMessages, spendTokens, saveConvo } = require('~/models'); const { retrieveAndProcessFile } = require('~/server/services/Files/process'); -const { recordMessage, getMessages } = require('~/models/Message'); -const { spendTokens } = require('~/models/spendTokens'); -const { saveConvo } = require('~/models/Conversation'); /** * Initializes a new thread or adds messages to an existing thread. @@ -62,24 +61,6 @@ async function initThread({ openai, body, thread_id: _thread_id }) { async function saveUserMessage(req, params) { const tokenCount = await countTokens(params.text); - // todo: do this on the frontend - // const { file_ids = [] } = params; - // let content; - // if (file_ids.length) { - // content = [ - // { - // value: params.text, - // }, - // ...( - // file_ids - // .filter(f => f) - // .map((file_id) => ({ - // file_id, - // })) - // ), - // ]; - // } - const userMessage = { user: params.user, endpoint: params.endpoint, @@ -110,9 +91,15 @@ async function saveUserMessage(req, params) { } const message = await recordMessage(userMessage); - await saveConvo(req, convo, { - context: 'api/server/services/Threads/manage.js #saveUserMessage', - }); + await saveConvo( + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, + convo, + { context: 'api/server/services/Threads/manage.js #saveUserMessage' }, + ); return message; } @@ -161,7 +148,11 @@ async function saveAssistantMessage(req, params) { }); await saveConvo( - req, + { + userId: req?.user?.id, + isTemporary: req?.body?.isTemporary, + interfaceConfig: req?.config?.interfaceConfig, + }, { endpoint: params.endpoint, conversationId: params.conversationId, @@ -353,7 +344,11 @@ async function syncMessages({ await Promise.all(recordPromises); await saveConvo( - openai.req, + { + userId: openai.req?.user?.id, + isTemporary: openai.req?.body?.isTemporary, + interfaceConfig: openai.req?.config?.interfaceConfig, + }, { conversationId, file_ids: attached_file_ids, diff --git a/api/server/services/cleanup.js b/api/server/services/cleanup.js index 7d3dfdec12..dc4f62c2ac 100644 --- a/api/server/services/cleanup.js +++ b/api/server/services/cleanup.js @@ -1,5 +1,5 @@ const { logger } = require('@librechat/data-schemas'); -const { deleteNullOrEmptyConversations } = require('~/models/Conversation'); +const { deleteNullOrEmptyConversations } = require('~/models'); const cleanup = async () => { try { diff --git a/api/server/services/start/migration.js b/api/server/services/start/migration.js index ab8d32b714..70f8300e08 100644 --- a/api/server/services/start/migration.js +++ b/api/server/services/start/migration.js @@ -6,7 +6,6 @@ const { checkAgentPermissionsMigration, checkPromptPermissionsMigration, } = require('@librechat/api'); -const { Agent, PromptGroup } = require('~/db/models'); const { findRoleByIdentifier } = require('~/models'); /** @@ -20,7 +19,7 @@ async function checkMigrations() { methods: { findRoleByIdentifier, }, - AgentModel: Agent, + AgentModel: mongoose.models.Agent, }); logAgentMigrationWarning(agentMigrationResult); } catch (error) { @@ -32,7 +31,7 @@ async function checkMigrations() { methods: { findRoleByIdentifier, }, - PromptGroupModel: PromptGroup, + PromptGroupModel: mongoose.models.PromptGroup, }); logPromptMigrationWarning(promptMigrationResult); } catch (error) { diff --git a/api/server/utils/import/fork.js b/api/server/utils/import/fork.js index f896de378c..5df4d27af2 100644 --- a/api/server/utils/import/fork.js +++ b/api/server/utils/import/fork.js @@ -3,8 +3,7 @@ const { logger } = require('@librechat/data-schemas'); const { EModelEndpoint, Constants, ForkOptions } = require('librechat-data-provider'); const { createImportBatchBuilder } = require('./importBatchBuilder'); const BaseClient = require('~/app/clients/BaseClient'); -const { getConvo } = require('~/models/Conversation'); -const { getMessages } = require('~/models/Message'); +const { getConvo, getMessages } = require('~/models'); /** * Helper function to clone messages with proper parent-child relationships and timestamps diff --git a/api/server/utils/import/fork.spec.js b/api/server/utils/import/fork.spec.js index 552620dc89..6fd108674a 100644 --- a/api/server/utils/import/fork.spec.js +++ b/api/server/utils/import/fork.spec.js @@ -1,16 +1,10 @@ const { Constants, ForkOptions } = require('librechat-data-provider'); -jest.mock('~/models/Conversation', () => ({ +jest.mock('~/models', () => ({ getConvo: jest.fn(), bulkSaveConvos: jest.fn(), -})); - -jest.mock('~/models/Message', () => ({ getMessages: jest.fn(), bulkSaveMessages: jest.fn(), -})); - -jest.mock('~/models/ConversationTag', () => ({ bulkIncrementTagCounts: jest.fn(), })); @@ -32,9 +26,13 @@ const { getMessagesUpToTargetLevel, cloneMessagesWithTimestamps, } = require('./fork'); -const { bulkIncrementTagCounts } = require('~/models/ConversationTag'); -const { getConvo, bulkSaveConvos } = require('~/models/Conversation'); -const { getMessages, bulkSaveMessages } = require('~/models/Message'); +const { + bulkIncrementTagCounts, + getConvo, + bulkSaveConvos, + getMessages, + bulkSaveMessages, +} = require('~/models'); const { createImportBatchBuilder } = require('./importBatchBuilder'); const BaseClient = require('~/app/clients/BaseClient'); diff --git a/api/server/utils/import/importBatchBuilder.js b/api/server/utils/import/importBatchBuilder.js index 5e499043d2..29fbfa85a2 100644 --- a/api/server/utils/import/importBatchBuilder.js +++ b/api/server/utils/import/importBatchBuilder.js @@ -1,9 +1,7 @@ const { v4: uuidv4 } = require('uuid'); const { logger } = require('@librechat/data-schemas'); const { EModelEndpoint, Constants, openAISettings } = require('librechat-data-provider'); -const { bulkIncrementTagCounts } = require('~/models/ConversationTag'); -const { bulkSaveConvos } = require('~/models/Conversation'); -const { bulkSaveMessages } = require('~/models/Message'); +const { bulkIncrementTagCounts, bulkSaveConvos, bulkSaveMessages } = require('~/models'); /** * Factory function for creating an instance of ImportBatchBuilder. diff --git a/api/server/utils/import/importers-timestamp.spec.js b/api/server/utils/import/importers-timestamp.spec.js index 02f24f72ae..09021a9ccd 100644 --- a/api/server/utils/import/importers-timestamp.spec.js +++ b/api/server/utils/import/importers-timestamp.spec.js @@ -4,10 +4,8 @@ const { ImportBatchBuilder } = require('./importBatchBuilder'); const { getImporter } = require('./importers'); // Mock the database methods -jest.mock('~/models/Conversation', () => ({ +jest.mock('~/models', () => ({ bulkSaveConvos: jest.fn(), -})); -jest.mock('~/models/Message', () => ({ bulkSaveMessages: jest.fn(), })); jest.mock('~/cache/getLogStores'); diff --git a/api/server/utils/import/importers.spec.js b/api/server/utils/import/importers.spec.js index 2ddfa76658..7984144cbc 100644 --- a/api/server/utils/import/importers.spec.js +++ b/api/server/utils/import/importers.spec.js @@ -1,10 +1,9 @@ const fs = require('fs'); const path = require('path'); const { EModelEndpoint, Constants, openAISettings } = require('librechat-data-provider'); -const { bulkSaveConvos: _bulkSaveConvos } = require('~/models/Conversation'); const { getImporter, processAssistantMessage } = require('./importers'); const { ImportBatchBuilder } = require('./importBatchBuilder'); -const { bulkSaveMessages } = require('~/models/Message'); +const { bulkSaveMessages, bulkSaveConvos: _bulkSaveConvos } = require('~/models'); const getLogStores = require('~/cache/getLogStores'); jest.mock('~/cache/getLogStores'); @@ -14,10 +13,8 @@ getLogStores.mockImplementation(() => ({ })); // Mock the database methods -jest.mock('~/models/Conversation', () => ({ +jest.mock('~/models', () => ({ bulkSaveConvos: jest.fn(), -})); -jest.mock('~/models/Message', () => ({ bulkSaveMessages: jest.fn(), })); diff --git a/api/strategies/localStrategy.js b/api/strategies/localStrategy.js index 0d220ead25..5d725c0907 100644 --- a/api/strategies/localStrategy.js +++ b/api/strategies/localStrategy.js @@ -1,8 +1,9 @@ +const bcrypt = require('bcryptjs'); const { logger } = require('@librechat/data-schemas'); const { errorsToString } = require('librechat-data-provider'); -const { isEnabled, checkEmailConfig } = require('@librechat/api'); const { Strategy: PassportLocalStrategy } = require('passport-local'); -const { findUser, comparePassword, updateUser } = require('~/models'); +const { isEnabled, checkEmailConfig, comparePassword } = require('@librechat/api'); +const { findUser, updateUser } = require('~/models'); const { loginSchema } = require('./validators'); // Unix timestamp for 2024-06-07 15:20:18 Eastern Time @@ -35,7 +36,7 @@ async function passportLogin(req, email, password, done) { return done(null, false, { message: 'Email does not exist.' }); } - const isMatch = await comparePassword(user, password); + const isMatch = await comparePassword(user, password, { compare: bcrypt.compare }); if (!isMatch) { logError('Passport Local Strategy - Password does not match', { isMatch }); logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`); diff --git a/api/test/services/Files/processFileCitations.test.js b/api/test/services/Files/processFileCitations.test.js index e9fe850ebd..8dd588afe9 100644 --- a/api/test/services/Files/processFileCitations.test.js +++ b/api/test/services/Files/processFileCitations.test.js @@ -7,12 +7,7 @@ const { // Mock dependencies jest.mock('~/models', () => ({ - Files: { - find: jest.fn().mockResolvedValue([]), - }, -})); - -jest.mock('~/models/Role', () => ({ + getFiles: jest.fn().mockResolvedValue([]), getRoleByName: jest.fn(), })); @@ -179,7 +174,7 @@ describe('processFileCitations', () => { }); describe('enhanceSourcesWithMetadata', () => { - const { Files } = require('~/models'); + const { getFiles } = require('~/models'); const mockCustomConfig = { fileStrategy: 'local', }; @@ -204,7 +199,7 @@ describe('processFileCitations', () => { }, ]; - Files.find.mockResolvedValue([ + getFiles.mockResolvedValue([ { file_id: 'file_123', filename: 'example_from_db.pdf', @@ -219,7 +214,7 @@ describe('processFileCitations', () => { const result = await enhanceSourcesWithMetadata(sources, mockCustomConfig); - expect(Files.find).toHaveBeenCalledWith({ file_id: { $in: ['file_123', 'file_456'] } }); + expect(getFiles).toHaveBeenCalledWith({ file_id: { $in: ['file_123', 'file_456'] } }); expect(result).toHaveLength(2); expect(result[0]).toEqual({ @@ -258,7 +253,7 @@ describe('processFileCitations', () => { }, ]; - Files.find.mockResolvedValue([ + getFiles.mockResolvedValue([ { file_id: 'file_123', filename: 'example_from_db.pdf', @@ -292,7 +287,7 @@ describe('processFileCitations', () => { }, ]; - Files.find.mockResolvedValue([]); + getFiles.mockResolvedValue([]); const result = await enhanceSourcesWithMetadata(sources, mockCustomConfig); @@ -317,7 +312,7 @@ describe('processFileCitations', () => { }, ]; - Files.find.mockRejectedValue(new Error('Database error')); + getFiles.mockRejectedValue(new Error('Database error')); const result = await enhanceSourcesWithMetadata(sources, mockCustomConfig); @@ -339,14 +334,14 @@ describe('processFileCitations', () => { { fileId: 'file_456', fileName: 'doc2.pdf', relevance: 0.7, type: 'file' }, ]; - Files.find.mockResolvedValue([ + getFiles.mockResolvedValue([ { file_id: 'file_123', filename: 'document1.pdf', source: 's3' }, { file_id: 'file_456', filename: 'document2.pdf', source: 'local' }, ]); await enhanceSourcesWithMetadata(sources, mockCustomConfig); - expect(Files.find).toHaveBeenCalledWith({ file_id: { $in: ['file_123', 'file_456'] } }); + expect(getFiles).toHaveBeenCalledWith({ file_id: { $in: ['file_123', 'file_456'] } }); }); }); }); diff --git a/api/models/PromptGroupMigration.spec.js b/config/__tests__/migrate-prompt-permissions.spec.js similarity index 98% rename from api/models/PromptGroupMigration.spec.js rename to config/__tests__/migrate-prompt-permissions.spec.js index 04ff612e7d..2d5b2cb4b0 100644 --- a/api/models/PromptGroupMigration.spec.js +++ b/config/__tests__/migrate-prompt-permissions.spec.js @@ -11,7 +11,7 @@ const { } = require('librechat-data-provider'); // Mock the config/connect module to prevent connection attempts during tests -jest.mock('../../config/connect', () => jest.fn().mockResolvedValue(true)); +jest.mock('../connect', () => jest.fn().mockResolvedValue(true)); // Disable console for tests logger.silent = true; @@ -78,7 +78,7 @@ describe('PromptGroup Migration Script', () => { }); // Import migration function - const migration = require('../../config/migrate-prompt-permissions'); + const migration = require('../migrate-prompt-permissions'); migrateToPromptGroupPermissions = migration.migrateToPromptGroupPermissions; }); diff --git a/config/add-balance.js b/config/add-balance.js index 0f86abb556..25de4c52e2 100644 --- a/config/add-balance.js +++ b/config/add-balance.js @@ -3,9 +3,9 @@ const mongoose = require('mongoose'); const { getBalanceConfig } = require('@librechat/api'); const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); -const { createTransaction } = require('~/models/Transaction'); const { getAppConfig } = require('~/server/services/Config'); const { askQuestion, silentExit } = require('./helpers'); +const { createTransaction } = require('~/models'); const connect = require('./connect'); (async () => { diff --git a/eslint.config.mjs b/eslint.config.mjs index bd848c7e3e..1dde65cda1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -355,5 +355,16 @@ export default [ project: './packages/data-schemas/tsconfig.json', }, }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + }, + ], + }, }, ]; diff --git a/packages/api/src/agents/__tests__/load.spec.ts b/packages/api/src/agents/__tests__/load.spec.ts new file mode 100644 index 0000000000..b7c6142d69 --- /dev/null +++ b/packages/api/src/agents/__tests__/load.spec.ts @@ -0,0 +1,397 @@ +import mongoose from 'mongoose'; +import { v4 as uuidv4 } from 'uuid'; +import { Constants } from 'librechat-data-provider'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { agentSchema, createMethods } from '@librechat/data-schemas'; +import type { AgentModelParameters } from 'librechat-data-provider'; +import type { LoadAgentParams, LoadAgentDeps } from '../load'; +import { loadAgent } from '../load'; + +let Agent: mongoose.Model; +let createAgent: ReturnType['createAgent']; +let getAgent: ReturnType['getAgent']; + +const mockGetMCPServerTools = jest.fn(); + +const deps: LoadAgentDeps = { + getAgent: (searchParameter) => getAgent(searchParameter), + getMCPServerTools: mockGetMCPServerTools, +}; + +describe('loadAgent', () => { + let mongoServer: MongoMemoryServer; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); + await mongoose.connect(mongoUri); + const methods = createMethods(mongoose); + createAgent = methods.createAgent; + getAgent = methods.getAgent; + }, 20000); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await Agent.deleteMany({}); + jest.clearAllMocks(); + }); + + test('should return null when agent_id is not provided', async () => { + const mockReq = { user: { id: 'user123' } }; + const result = await loadAgent( + { + req: mockReq, + agent_id: null as unknown as string, + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + expect(result).toBeNull(); + }); + + test('should return null when agent_id is empty string', async () => { + const mockReq = { user: { id: 'user123' } }; + const result = await loadAgent( + { + req: mockReq, + agent_id: '', + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + expect(result).toBeNull(); + }); + + test('should test ephemeral agent loading logic', async () => { + const { EPHEMERAL_AGENT_ID } = Constants; + + // Mock getMCPServerTools to return tools for each server + mockGetMCPServerTools.mockImplementation(async (_userId: string, server: string) => { + if (server === 'server1') { + return { tool1_mcp_server1: {} }; + } else if (server === 'server2') { + return { tool2_mcp_server2: {} }; + } + return null; + }); + + const mockReq = { + user: { id: 'user123' }, + body: { + promptPrefix: 'Test instructions', + ephemeralAgent: { + execute_code: true, + web_search: true, + mcp: ['server1', 'server2'], + }, + }, + }; + + const result = await loadAgent( + { + req: mockReq, + agent_id: EPHEMERAL_AGENT_ID as string, + endpoint: 'openai', + model_parameters: { model: 'gpt-4', temperature: 0.7 } as unknown as AgentModelParameters, + }, + deps, + ); + + if (result) { + // Ephemeral agent ID is encoded with endpoint and model + expect(result.id).toBe('openai__gpt-4'); + expect(result.instructions).toBe('Test instructions'); + expect(result.provider).toBe('openai'); + expect(result.model).toBe('gpt-4'); + expect(result.model_parameters.temperature).toBe(0.7); + expect(result.tools).toContain('execute_code'); + expect(result.tools).toContain('web_search'); + expect(result.tools).toContain('tool1_mcp_server1'); + expect(result.tools).toContain('tool2_mcp_server2'); + } else { + expect(result).toBeNull(); + } + }); + + test('should return null for non-existent agent', async () => { + const mockReq = { user: { id: 'user123' } }; + const result = await loadAgent( + { + req: mockReq, + agent_id: 'agent_non_existent', + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + expect(result).toBeNull(); + }); + + test('should load agent when user is the author', async () => { + const userId = new mongoose.Types.ObjectId(); + const agentId = `agent_${uuidv4()}`; + + await createAgent({ + id: agentId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: userId, + description: 'Test description', + tools: ['web_search'], + }); + + const mockReq = { user: { id: userId.toString() } }; + const result = await loadAgent( + { + req: mockReq, + agent_id: agentId, + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + expect(result).toBeDefined(); + expect(result!.id).toBe(agentId); + expect(result!.name).toBe('Test Agent'); + expect(String(result!.author)).toBe(userId.toString()); + expect(result!.version).toBe(1); + }); + + test('should return agent even when user is not author (permissions checked at route level)', async () => { + const authorId = new mongoose.Types.ObjectId(); + const userId = new mongoose.Types.ObjectId(); + const agentId = `agent_${uuidv4()}`; + + await createAgent({ + id: agentId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const mockReq = { user: { id: userId.toString() } }; + const result = await loadAgent( + { + req: mockReq, + agent_id: agentId, + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + // With the new permission system, loadAgent returns the agent regardless of permissions + // Permission checks are handled at the route level via middleware + expect(result).toBeTruthy(); + expect(result!.id).toBe(agentId); + expect(result!.name).toBe('Test Agent'); + }); + + test('should handle ephemeral agent with no MCP servers', async () => { + const { EPHEMERAL_AGENT_ID } = Constants; + + const mockReq = { + user: { id: 'user123' }, + body: { + promptPrefix: 'Simple instructions', + ephemeralAgent: { + execute_code: false, + web_search: false, + mcp: [], + }, + }, + }; + + const result = await loadAgent( + { + req: mockReq, + agent_id: EPHEMERAL_AGENT_ID as string, + endpoint: 'openai', + model_parameters: { model: 'gpt-3.5-turbo' } as unknown as AgentModelParameters, + }, + deps, + ); + + if (result) { + expect(result.tools).toEqual([]); + expect(result.instructions).toBe('Simple instructions'); + } else { + expect(result).toBeFalsy(); + } + }); + + test('should handle ephemeral agent with undefined ephemeralAgent in body', async () => { + const { EPHEMERAL_AGENT_ID } = Constants; + + const mockReq = { + user: { id: 'user123' }, + body: { + promptPrefix: 'Basic instructions', + }, + }; + + const result = await loadAgent( + { + req: mockReq, + agent_id: EPHEMERAL_AGENT_ID as string, + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + if (result) { + expect(result.tools).toEqual([]); + } else { + expect(result).toBeFalsy(); + } + }); + + describe('Edge Cases', () => { + test('should handle loadAgent with malformed req object', async () => { + const result = await loadAgent( + { + req: null as unknown as LoadAgentParams['req'], + agent_id: 'agent_test', + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + expect(result).toBeNull(); + }); + + test('should handle ephemeral agent with extremely large tool list', async () => { + const { EPHEMERAL_AGENT_ID } = Constants; + + const largeToolList = Array.from({ length: 100 }, (_, i) => `tool_${i}_mcp_server1`); + const availableTools: Record = {}; + for (const tool of largeToolList) { + availableTools[tool] = {}; + } + + // Mock getMCPServerTools to return all tools for server1 + mockGetMCPServerTools.mockImplementation(async (_userId: string, server: string) => { + if (server === 'server1') { + return availableTools; // All 100 tools belong to server1 + } + return null; + }); + + const mockReq = { + user: { id: 'user123' }, + body: { + promptPrefix: 'Test', + ephemeralAgent: { + execute_code: true, + web_search: true, + mcp: ['server1'], + }, + }, + }; + + const result = await loadAgent( + { + req: mockReq, + agent_id: EPHEMERAL_AGENT_ID as string, + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + if (result) { + expect(result.tools!.length).toBeGreaterThan(100); + } + }); + + test('should return agent from different project (permissions checked at route level)', async () => { + const authorId = new mongoose.Types.ObjectId(); + const userId = new mongoose.Types.ObjectId(); + const agentId = `agent_${uuidv4()}`; + + await createAgent({ + id: agentId, + name: 'Project Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const mockReq = { user: { id: userId.toString() } }; + const result = await loadAgent( + { + req: mockReq, + agent_id: agentId, + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + // With the new permission system, loadAgent returns the agent regardless of permissions + // Permission checks are handled at the route level via middleware + expect(result).toBeTruthy(); + expect(result!.id).toBe(agentId); + expect(result!.name).toBe('Project Agent'); + }); + + test('should handle loadEphemeralAgent with malformed MCP tool names', async () => { + const { EPHEMERAL_AGENT_ID } = Constants; + + // Mock getMCPServerTools to return only tools matching the server + mockGetMCPServerTools.mockImplementation(async (_userId: string, server: string) => { + if (server === 'server1') { + // Only return tool that correctly matches server1 format + return { tool_mcp_server1: {} }; + } else if (server === 'server2') { + return { tool_mcp_server2: {} }; + } + return null; + }); + + const mockReq = { + user: { id: 'user123' }, + body: { + promptPrefix: 'Test instructions', + ephemeralAgent: { + execute_code: false, + web_search: false, + mcp: ['server1'], + }, + }, + }; + + const result = await loadAgent( + { + req: mockReq, + agent_id: EPHEMERAL_AGENT_ID as string, + endpoint: 'openai', + model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters, + }, + deps, + ); + + if (result) { + expect(result.tools).toEqual(['tool_mcp_server1']); + expect(result.tools).not.toContain('malformed_tool_name'); + expect(result.tools).not.toContain('tool__server1'); + expect(result.tools).not.toContain('tool_mcp_server2'); + } + }); + }); +}); diff --git a/packages/api/src/agents/added.ts b/packages/api/src/agents/added.ts new file mode 100644 index 0000000000..587f3bc437 --- /dev/null +++ b/packages/api/src/agents/added.ts @@ -0,0 +1,230 @@ +import { logger } from '@librechat/data-schemas'; +import type { AppConfig } from '@librechat/data-schemas'; +import { + Tools, + Constants, + isAgentsEndpoint, + isEphemeralAgentId, + appendAgentIdSuffix, + encodeEphemeralAgentId, +} from 'librechat-data-provider'; +import type { Agent, TConversation } from 'librechat-data-provider'; +import { getCustomEndpointConfig } from '~/app/config'; + +const { mcp_all, mcp_delimiter } = Constants; + +export const ADDED_AGENT_ID = 'added_agent'; + +export interface LoadAddedAgentDeps { + getAgent: (searchParameter: { id: string }) => Promise; + getMCPServerTools: ( + userId: string, + serverName: string, + ) => Promise | null>; +} + +interface LoadAddedAgentParams { + req: { user?: { id?: string }; config?: Record }; + conversation: TConversation | null; + primaryAgent?: Agent | null; +} + +/** + * Loads an agent from an added conversation (for multi-convo parallel agent execution). + * Returns the agent config as a plain object, or null if invalid. + */ +export async function loadAddedAgent( + { req, conversation, primaryAgent }: LoadAddedAgentParams, + deps: LoadAddedAgentDeps, +): Promise { + if (!conversation) { + return null; + } + + if (conversation.agent_id && !isEphemeralAgentId(conversation.agent_id)) { + const reqRecord = req as Record; + let agent = reqRecord.resolvedAddedAgent as Agent | null | undefined; + if (!agent) { + agent = await deps.getAgent({ id: conversation.agent_id }); + } + if (!agent) { + logger.warn(`[loadAddedAgent] Agent ${conversation.agent_id} not found`); + return null; + } + + const agentRecord = agent as Record; + const versions = agentRecord.versions as unknown[] | undefined; + agentRecord.version = versions ? versions.length : 0; + agent.id = appendAgentIdSuffix(agent.id, 1); + return agent; + } + + const { model, endpoint, promptPrefix, spec, ...rest } = conversation as TConversation & { + promptPrefix?: string; + spec?: string; + modelLabel?: string; + ephemeralAgent?: { + mcp?: string[]; + execute_code?: boolean; + file_search?: boolean; + web_search?: boolean; + artifacts?: unknown; + }; + [key: string]: unknown; + }; + + if (!endpoint || !model) { + logger.warn('[loadAddedAgent] Missing required endpoint or model for ephemeral agent'); + return null; + } + + const appConfig = req.config as AppConfig | undefined; + + const primaryIsEphemeral = primaryAgent && isEphemeralAgentId(primaryAgent.id); + if (primaryIsEphemeral && Array.isArray(primaryAgent.tools)) { + let endpointConfig = (appConfig?.endpoints as Record | undefined)?.[ + endpoint + ] as Record | undefined; + if (!isAgentsEndpoint(endpoint) && !endpointConfig) { + try { + endpointConfig = getCustomEndpointConfig({ endpoint, appConfig }) as + | Record + | undefined; + } catch (err) { + logger.error('[loadAddedAgent] Error getting custom endpoint config', err); + } + } + + const modelSpecs = (appConfig?.modelSpecs as { list?: Array<{ name: string; label?: string }> }) + ?.list; + const modelSpec = spec != null && spec !== '' ? modelSpecs?.find((s) => s.name === spec) : null; + const sender = + rest.modelLabel ?? + modelSpec?.label ?? + (endpointConfig?.modelDisplayLabel as string | undefined) ?? + ''; + const ephemeralId = encodeEphemeralAgentId({ endpoint, model, sender, index: 1 }); + + return { + id: ephemeralId, + instructions: promptPrefix || '', + provider: endpoint, + model_parameters: {}, + model, + tools: [...primaryAgent.tools], + } as unknown as Agent; + } + + const ephemeralAgent = rest.ephemeralAgent as + | { + mcp?: string[]; + execute_code?: boolean; + file_search?: boolean; + web_search?: boolean; + artifacts?: unknown; + } + | undefined; + const mcpServers = new Set(ephemeralAgent?.mcp); + const userId = req.user?.id ?? ''; + + const modelSpecs = ( + appConfig?.modelSpecs as { + list?: Array<{ + name: string; + label?: string; + mcpServers?: string[]; + executeCode?: boolean; + fileSearch?: boolean; + webSearch?: boolean; + }>; + } + )?.list; + let modelSpec: (typeof modelSpecs extends Array | undefined ? T : never) | null = null; + if (spec != null && spec !== '') { + modelSpec = modelSpecs?.find((s) => s.name === spec) ?? null; + } + if (modelSpec?.mcpServers) { + for (const mcpServer of modelSpec.mcpServers) { + mcpServers.add(mcpServer); + } + } + + const tools: string[] = []; + if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) { + tools.push(Tools.execute_code); + } + if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) { + tools.push(Tools.file_search); + } + if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) { + tools.push(Tools.web_search); + } + + const addedServers = new Set(); + for (const mcpServer of mcpServers) { + if (addedServers.has(mcpServer)) { + continue; + } + const serverTools = await deps.getMCPServerTools(userId, mcpServer); + if (!serverTools) { + tools.push(`${mcp_all}${mcp_delimiter}${mcpServer}`); + addedServers.add(mcpServer); + continue; + } + tools.push(...Object.keys(serverTools)); + addedServers.add(mcpServer); + } + + const model_parameters: Record = {}; + const paramKeys = [ + 'temperature', + 'top_p', + 'topP', + 'topK', + 'presence_penalty', + 'frequency_penalty', + 'maxOutputTokens', + 'maxTokens', + 'max_tokens', + ]; + for (const key of paramKeys) { + if ((rest as Record)[key] != null) { + model_parameters[key] = (rest as Record)[key]; + } + } + + let endpointConfig = (appConfig?.endpoints as Record | undefined)?.[endpoint] as + | Record + | undefined; + if (!isAgentsEndpoint(endpoint) && !endpointConfig) { + try { + endpointConfig = getCustomEndpointConfig({ endpoint, appConfig }) as + | Record + | undefined; + } catch (err) { + logger.error('[loadAddedAgent] Error getting custom endpoint config', err); + } + } + + const sender = + rest.modelLabel ?? + modelSpec?.label ?? + (endpointConfig?.modelDisplayLabel as string | undefined) ?? + ''; + const ephemeralId = encodeEphemeralAgentId({ endpoint, model, sender, index: 1 }); + + const result: Record = { + id: ephemeralId, + instructions: promptPrefix || '', + provider: endpoint, + model_parameters, + model, + tools, + }; + + if (ephemeralAgent?.artifacts != null && ephemeralAgent.artifacts) { + result.artifacts = ephemeralAgent.artifacts; + } + + return result as unknown as Agent; +} diff --git a/packages/api/src/agents/index.ts b/packages/api/src/agents/index.ts index 47e15b8c28..ffb8cec332 100644 --- a/packages/api/src/agents/index.ts +++ b/packages/api/src/agents/index.ts @@ -16,3 +16,5 @@ export * from './responses'; export * from './run'; export * from './tools'; export * from './validation'; +export * from './added'; +export * from './load'; diff --git a/packages/api/src/agents/load.ts b/packages/api/src/agents/load.ts new file mode 100644 index 0000000000..05746d1195 --- /dev/null +++ b/packages/api/src/agents/load.ts @@ -0,0 +1,162 @@ +import { logger } from '@librechat/data-schemas'; +import type { AppConfig } from '@librechat/data-schemas'; +import { + Tools, + Constants, + isAgentsEndpoint, + isEphemeralAgentId, + encodeEphemeralAgentId, +} from 'librechat-data-provider'; +import type { + AgentModelParameters, + TEphemeralAgent, + TModelSpec, + Agent, +} from 'librechat-data-provider'; +import { getCustomEndpointConfig } from '~/app/config'; + +const { mcp_all, mcp_delimiter } = Constants; + +export interface LoadAgentDeps { + getAgent: (searchParameter: { id: string }) => Promise; + getMCPServerTools: ( + userId: string, + serverName: string, + ) => Promise | null>; +} + +export interface LoadAgentParams { + req: { + user?: { id?: string }; + config?: AppConfig; + body?: { + promptPrefix?: string; + ephemeralAgent?: TEphemeralAgent; + }; + }; + spec?: string; + agent_id: string; + endpoint: string; + model_parameters?: AgentModelParameters & { model?: string }; +} + +/** + * Load an ephemeral agent based on the request parameters. + */ +export async function loadEphemeralAgent( + { req, spec, endpoint, model_parameters: _m }: Omit, + deps: LoadAgentDeps, +): Promise { + const { model, ...model_parameters } = _m ?? ({} as unknown as AgentModelParameters); + const modelSpecs = req.config?.modelSpecs as { list?: TModelSpec[] } | undefined; + let modelSpec: TModelSpec | null = null; + if (spec != null && spec !== '') { + modelSpec = modelSpecs?.list?.find((s) => s.name === spec) ?? null; + } + const ephemeralAgent: TEphemeralAgent | undefined = req.body?.ephemeralAgent; + const mcpServers = new Set(ephemeralAgent?.mcp); + const userId = req.user?.id ?? ''; + if (modelSpec?.mcpServers) { + for (const mcpServer of modelSpec.mcpServers) { + mcpServers.add(mcpServer); + } + } + const tools: string[] = []; + if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) { + tools.push(Tools.execute_code); + } + if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) { + tools.push(Tools.file_search); + } + if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) { + tools.push(Tools.web_search); + } + + const addedServers = new Set(); + if (mcpServers.size > 0) { + for (const mcpServer of mcpServers) { + if (addedServers.has(mcpServer)) { + continue; + } + const serverTools = await deps.getMCPServerTools(userId, mcpServer); + if (!serverTools) { + tools.push(`${mcp_all}${mcp_delimiter}${mcpServer}`); + addedServers.add(mcpServer); + continue; + } + tools.push(...Object.keys(serverTools)); + addedServers.add(mcpServer); + } + } + + const instructions = req.body?.promptPrefix; + + // Get endpoint config for modelDisplayLabel fallback + const appConfig = req.config; + const endpoints = appConfig?.endpoints; + let endpointConfig = endpoints?.[endpoint as keyof typeof endpoints]; + if (!isAgentsEndpoint(endpoint) && !endpointConfig) { + try { + endpointConfig = getCustomEndpointConfig({ endpoint, appConfig }); + } catch (err) { + logger.error('[loadEphemeralAgent] Error getting custom endpoint config', err); + } + } + + // For ephemeral agents, use modelLabel if provided, then model spec's label, + // then modelDisplayLabel from endpoint config, otherwise empty string to show model name + const sender = + (model_parameters as AgentModelParameters & { modelLabel?: string })?.modelLabel ?? + modelSpec?.label ?? + (endpointConfig as { modelDisplayLabel?: string } | undefined)?.modelDisplayLabel ?? + ''; + + // Encode ephemeral agent ID with endpoint, model, and computed sender for display + const ephemeralId = encodeEphemeralAgentId({ + endpoint, + model: model as string, + sender: sender as string, + }); + + const result: Partial = { + id: ephemeralId, + instructions, + provider: endpoint, + model_parameters, + model, + tools, + }; + + if (ephemeralAgent?.artifacts) { + result.artifacts = ephemeralAgent.artifacts; + } + return result as Agent; +} + +/** + * Load an agent based on the provided ID. + * For ephemeral agents, builds a synthetic agent from request parameters. + * For persistent agents, fetches from the database. + */ +export async function loadAgent( + params: LoadAgentParams, + deps: LoadAgentDeps, +): Promise { + const { req, spec, agent_id, endpoint, model_parameters } = params; + if (!agent_id) { + return null; + } + if (isEphemeralAgentId(agent_id)) { + return loadEphemeralAgent({ req, spec, endpoint, model_parameters }, deps); + } + const agent = await deps.getAgent({ id: agent_id }); + + if (!agent) { + return null; + } + + // Set version count from versions array length + const agentWithVersion = agent as Agent & { versions?: unknown[]; version?: number }; + agentWithVersion.version = agentWithVersion.versions ? agentWithVersion.versions.length : 0; + return agent; +} diff --git a/packages/api/src/apiKeys/permissions.ts b/packages/api/src/apiKeys/permissions.ts index 2556f25b57..b617b0a892 100644 --- a/packages/api/src/apiKeys/permissions.ts +++ b/packages/api/src/apiKeys/permissions.ts @@ -1,10 +1,11 @@ +import { Types } from 'mongoose'; import { ResourceType, PrincipalType, PermissionBits, AccessRoleIds, } from 'librechat-data-provider'; -import type { Types, Model } from 'mongoose'; +import type { PipelineStage, AnyBulkWriteOperation } from 'mongoose'; export interface Principal { type: string; @@ -19,20 +20,14 @@ export interface Principal { } export interface EnricherDependencies { - AclEntry: Model<{ - principalType: string; - principalId: Types.ObjectId; - resourceType: string; - resourceId: Types.ObjectId; - permBits: number; - roleId: Types.ObjectId; - grantedBy: Types.ObjectId; - grantedAt: Date; - }>; - AccessRole: Model<{ - accessRoleId: string; - permBits: number; - }>; + aggregateAclEntries: (pipeline: PipelineStage[]) => Promise[]>; + bulkWriteAclEntries: ( + ops: AnyBulkWriteOperation[], + options?: Record, + ) => Promise; + findRoleByIdentifier: ( + accessRoleId: string, + ) => Promise<{ _id: Types.ObjectId; permBits: number } | null>; logger: { error: (msg: string, ...args: unknown[]) => void }; } @@ -47,14 +42,12 @@ export async function enrichRemoteAgentPrincipals( resourceId: string | Types.ObjectId, principals: Principal[], ): Promise { - const { AclEntry } = deps; - const resourceObjectId = typeof resourceId === 'string' && /^[a-f\d]{24}$/i.test(resourceId) - ? deps.AclEntry.base.Types.ObjectId.createFromHexString(resourceId) + ? Types.ObjectId.createFromHexString(resourceId) : resourceId; - const agentOwnerEntries = await AclEntry.aggregate([ + const agentOwnerEntries = await deps.aggregateAclEntries([ { $match: { resourceType: ResourceType.AGENT, @@ -87,24 +80,28 @@ export async function enrichRemoteAgentPrincipals( continue; } + const userInfo = entry.userInfo as Record; + const principalId = entry.principalId as Types.ObjectId; + const alreadyIncluded = enrichedPrincipals.some( - (p) => p.type === PrincipalType.USER && p.id === entry.principalId.toString(), + (p) => p.type === PrincipalType.USER && p.id === principalId.toString(), ); if (!alreadyIncluded) { enrichedPrincipals.unshift({ type: PrincipalType.USER, - id: entry.userInfo._id.toString(), - name: entry.userInfo.name || entry.userInfo.username, - email: entry.userInfo.email, - avatar: entry.userInfo.avatar, + id: (userInfo._id as Types.ObjectId).toString(), + name: (userInfo.name || userInfo.username) as string, + email: userInfo.email as string, + avatar: userInfo.avatar as string, source: 'local', - idOnTheSource: entry.userInfo.idOnTheSource || entry.userInfo._id.toString(), + idOnTheSource: + (userInfo.idOnTheSource as string) || (userInfo._id as Types.ObjectId).toString(), accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, isImplicit: true, }); - entriesToBackfill.push(entry.principalId); + entriesToBackfill.push(principalId); } } @@ -121,15 +118,15 @@ export function backfillRemoteAgentPermissions( return; } - const { AclEntry, AccessRole, logger } = deps; + const { logger } = deps; const resourceObjectId = typeof resourceId === 'string' && /^[a-f\d]{24}$/i.test(resourceId) - ? AclEntry.base.Types.ObjectId.createFromHexString(resourceId) + ? Types.ObjectId.createFromHexString(resourceId) : resourceId; - AccessRole.findOne({ accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER }) - .lean() + deps + .findRoleByIdentifier(AccessRoleIds.REMOTE_AGENT_OWNER) .then((role) => { if (!role) { logger.error('[backfillRemoteAgentPermissions] REMOTE_AGENT_OWNER role not found'); @@ -161,9 +158,9 @@ export function backfillRemoteAgentPermissions( }, })); - return AclEntry.bulkWrite(bulkOps, { ordered: false }); + return deps.bulkWriteAclEntries(bulkOps, { ordered: false }); }) - .catch((err) => { + .catch((err: unknown) => { logger.error('[backfillRemoteAgentPermissions] Failed to backfill:', err); }); } diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts index 392605ef50..5dd0bb01e0 100644 --- a/packages/api/src/auth/index.ts +++ b/packages/api/src/auth/index.ts @@ -2,3 +2,5 @@ export * from './domain'; export * from './openid'; export * from './exchange'; export * from './agent'; +export * from './password'; +export * from './invite'; diff --git a/packages/api/src/auth/invite.ts b/packages/api/src/auth/invite.ts new file mode 100644 index 0000000000..19e1e54b46 --- /dev/null +++ b/packages/api/src/auth/invite.ts @@ -0,0 +1,61 @@ +import { Types } from 'mongoose'; +import { logger, hashToken, getRandomValues } from '@librechat/data-schemas'; + +export interface InviteDeps { + createToken: (data: { + userId: Types.ObjectId; + email: string; + token: string; + createdAt: number; + expiresIn: number; + }) => Promise; + findToken: (filter: { token: string; email: string }) => Promise; +} + +/** Creates a new user invite and returns the encoded token. */ +export async function createInvite( + email: string, + deps: InviteDeps, +): Promise { + try { + const token = await getRandomValues(32); + const hash = await hashToken(token); + const encodedToken = encodeURIComponent(token); + const fakeUserId = new Types.ObjectId(); + + await deps.createToken({ + userId: fakeUserId, + email, + token: hash, + createdAt: Date.now(), + expiresIn: 604800, + }); + + return encodedToken; + } catch (error) { + logger.error('[createInvite] Error creating invite', error); + return { message: 'Error creating invite' }; + } +} + +/** Retrieves and validates a user invite by encoded token and email. */ +export async function getInvite( + encodedToken: string, + email: string, + deps: InviteDeps, +): Promise { + try { + const token = decodeURIComponent(encodedToken); + const hash = await hashToken(token); + const invite = await deps.findToken({ token: hash, email }); + + if (!invite) { + throw new Error('Invite not found or email does not match'); + } + + return invite; + } catch (error) { + logger.error('[getInvite] Error getting invite:', error); + return { error: true, message: (error as Error).message }; + } +} diff --git a/packages/api/src/auth/password.ts b/packages/api/src/auth/password.ts new file mode 100644 index 0000000000..87eea94d7e --- /dev/null +++ b/packages/api/src/auth/password.ts @@ -0,0 +1,25 @@ +interface UserWithPassword { + password?: string; + [key: string]: unknown; +} + +export interface ComparePasswordDeps { + compare: (candidatePassword: string, hash: string) => Promise; +} + +/** Compares a candidate password against a user's hashed password. */ +export async function comparePassword( + user: UserWithPassword, + candidatePassword: string, + deps: ComparePasswordDeps, +): Promise { + if (!user) { + throw new Error('No user provided'); + } + + if (!user.password) { + throw new Error('No password, likely an email first registered via Social/OIDC login'); + } + + return deps.compare(candidatePassword, user.password); +} diff --git a/packages/api/src/mcp/registry/db/ServerConfigsDB.ts b/packages/api/src/mcp/registry/db/ServerConfigsDB.ts index 50db81b831..9981f6b00b 100644 --- a/packages/api/src/mcp/registry/db/ServerConfigsDB.ts +++ b/packages/api/src/mcp/registry/db/ServerConfigsDB.ts @@ -367,12 +367,12 @@ export class ServerConfigsDB implements IServerConfigsRepositoryInterface { const parsedConfigs: Record = {}; const directData = directResults.data || []; - const directServerNames = new Set(directData.map((s) => s.serverName)); + const directServerNames = new Set(directData.map((s: MCPServerDocument) => s.serverName)); const directParsed = await Promise.all( - directData.map((s) => this.mapDBServerToParsedConfig(s)), + directData.map((s: MCPServerDocument) => this.mapDBServerToParsedConfig(s)), ); - directData.forEach((s, i) => { + directData.forEach((s: MCPServerDocument, i: number) => { parsedConfigs[s.serverName] = directParsed[i]; }); @@ -385,9 +385,9 @@ export class ServerConfigsDB implements IServerConfigsRepositoryInterface { const agentData = agentServers.data || []; const agentParsed = await Promise.all( - agentData.map((s) => this.mapDBServerToParsedConfig(s)), + agentData.map((s: MCPServerDocument) => this.mapDBServerToParsedConfig(s)), ); - agentData.forEach((s, i) => { + agentData.forEach((s: MCPServerDocument, i: number) => { parsedConfigs[s.serverName] = { ...agentParsed[i], consumeOnly: true }; }); } diff --git a/packages/api/src/middleware/access.spec.ts b/packages/api/src/middleware/access.spec.ts index c0efa9fcc1..99257adf6d 100644 --- a/packages/api/src/middleware/access.spec.ts +++ b/packages/api/src/middleware/access.spec.ts @@ -216,12 +216,12 @@ describe('access middleware', () => { defaultParams.getRoleByName.mockResolvedValue(mockRole); - const checkObject = {}; + const checkObject = { id: 'agent123' }; const result = await checkAccess({ ...defaultParams, permissions: [Permissions.USE, Permissions.SHARE], - bodyProps: {} as Record, + bodyProps: { [Permissions.SHARE]: ['id'] } as Record, checkObject, }); expect(result).toBe(true); @@ -333,12 +333,12 @@ describe('access middleware', () => { } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); - mockReq.body = {}; + mockReq.body = { id: 'agent123' }; const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARE], - bodyProps: {} as Record, + bodyProps: { [Permissions.SHARE]: ['id'] } as Record, getRoleByName: mockGetRoleByName, }); diff --git a/packages/api/src/middleware/balance.spec.ts b/packages/api/src/middleware/balance.spec.ts index 076ec6d519..fe995d9f6b 100644 --- a/packages/api/src/middleware/balance.spec.ts +++ b/packages/api/src/middleware/balance.spec.ts @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; import { logger, balanceSchema } from '@librechat/data-schemas'; import type { NextFunction, Request as ServerRequest, Response as ServerResponse } from 'express'; -import type { IBalance } from '@librechat/data-schemas'; +import type { IBalance, IBalanceUpdate } from '@librechat/data-schemas'; import { createSetBalanceConfig } from './balance'; jest.mock('@librechat/data-schemas', () => ({ @@ -15,6 +15,16 @@ jest.mock('@librechat/data-schemas', () => ({ let mongoServer: MongoMemoryServer; let Balance: mongoose.Model; +const findBalanceByUser = (userId: string) => + Balance.findOne({ user: userId }).lean() as Promise; + +const upsertBalanceFields = (userId: string, fields: IBalanceUpdate) => + Balance.findOneAndUpdate( + { user: userId }, + { $set: fields }, + { upsert: true, new: true }, + ).lean() as Promise; + beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); @@ -64,7 +74,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -95,7 +106,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -120,7 +132,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -149,7 +162,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -178,7 +192,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = {} as ServerRequest; @@ -219,7 +234,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -271,7 +287,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -315,7 +332,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -346,7 +364,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -392,7 +411,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -434,21 +454,20 @@ describe('createSetBalanceConfig', () => { }, }); - const middleware = createSetBalanceConfig({ - getAppConfig, - Balance, - }); - const req = createMockRequest(userId); const res = createMockResponse(); - // Spy on Balance.findOneAndUpdate to verify it's not called - const updateSpy = jest.spyOn(Balance, 'findOneAndUpdate'); + const upsertSpy = jest.fn(); + const spiedMiddleware = createSetBalanceConfig({ + getAppConfig, + findBalanceByUser, + upsertBalanceFields: upsertSpy, + }); - await middleware(req as ServerRequest, res as ServerResponse, mockNext); + await spiedMiddleware(req as ServerRequest, res as ServerResponse, mockNext); expect(mockNext).toHaveBeenCalled(); - expect(updateSpy).not.toHaveBeenCalled(); + expect(upsertSpy).not.toHaveBeenCalled(); }); test('should set tokenCredits for user with null tokenCredits', async () => { @@ -470,7 +489,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -498,16 +518,12 @@ describe('createSetBalanceConfig', () => { }); const dbError = new Error('Database error'); - // Mock Balance.findOne to throw an error - jest.spyOn(Balance, 'findOne').mockImplementationOnce((() => { - return { - lean: jest.fn().mockRejectedValue(dbError), - }; - }) as unknown as mongoose.Model['findOne']); + const failingFindBalance = () => Promise.reject(dbError); const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser: failingFindBalance, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -526,7 +542,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -556,7 +573,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -590,7 +608,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); @@ -635,7 +654,8 @@ describe('createSetBalanceConfig', () => { const middleware = createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }); const req = createMockRequest(userId); diff --git a/packages/api/src/middleware/balance.ts b/packages/api/src/middleware/balance.ts index e3eb1e7ae1..8c6b149cdd 100644 --- a/packages/api/src/middleware/balance.ts +++ b/packages/api/src/middleware/balance.ts @@ -1,13 +1,20 @@ import { logger } from '@librechat/data-schemas'; +import type { + IBalanceUpdate, + BalanceConfig, + AppConfig, + ObjectId, + IBalance, + IUser, +} from '@librechat/data-schemas'; import type { NextFunction, Request as ServerRequest, Response as ServerResponse } from 'express'; -import type { IBalance, IUser, BalanceConfig, ObjectId, AppConfig } from '@librechat/data-schemas'; -import type { Model } from 'mongoose'; import type { BalanceUpdateFields } from '~/types'; import { getBalanceConfig } from '~/app/config'; export interface BalanceMiddlewareOptions { getAppConfig: (options?: { role?: string; refresh?: boolean }) => Promise; - Balance: Model; + findBalanceByUser: (userId: string) => Promise; + upsertBalanceFields: (userId: string, fields: IBalanceUpdate) => Promise; } /** @@ -75,7 +82,8 @@ function buildUpdateFields( */ export function createSetBalanceConfig({ getAppConfig, - Balance, + findBalanceByUser, + upsertBalanceFields, }: BalanceMiddlewareOptions): ( req: ServerRequest, res: ServerResponse, @@ -97,18 +105,14 @@ export function createSetBalanceConfig({ return next(); } const userId = typeof user._id === 'string' ? user._id : user._id.toString(); - const userBalanceRecord = await Balance.findOne({ user: userId }).lean(); + const userBalanceRecord = await findBalanceByUser(userId); const updateFields = buildUpdateFields(balanceConfig, userBalanceRecord, userId); if (Object.keys(updateFields).length === 0) { return next(); } - await Balance.findOneAndUpdate( - { user: userId }, - { $set: updateFields }, - { upsert: true, new: true }, - ); + await upsertBalanceFields(userId, updateFields); next(); } catch (error) { diff --git a/packages/api/src/middleware/checkBalance.ts b/packages/api/src/middleware/checkBalance.ts new file mode 100644 index 0000000000..d99874dc07 --- /dev/null +++ b/packages/api/src/middleware/checkBalance.ts @@ -0,0 +1,168 @@ +import { logger } from '@librechat/data-schemas'; +import { ViolationTypes } from 'librechat-data-provider'; +import type { ServerRequest } from '~/types/http'; +import type { Response } from 'express'; + +type TimeUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months'; + +interface BalanceRecord { + tokenCredits: number; + autoRefillEnabled?: boolean; + refillAmount?: number; + lastRefill?: Date; + refillIntervalValue?: number; + refillIntervalUnit?: TimeUnit; +} + +interface TxData { + user: string; + model?: string; + endpoint?: string; + valueKey?: string; + tokenType?: string; + amount: number; + endpointTokenConfig?: unknown; + generations?: unknown[]; +} + +export interface CheckBalanceDeps { + findBalanceByUser: (user: string) => Promise; + getMultiplier: (params: Record) => number; + createAutoRefillTransaction: ( + data: Record, + ) => Promise<{ balance: number } | undefined>; + logViolation: ( + req: unknown, + res: unknown, + type: string, + errorMessage: Record, + score: number, + ) => Promise; +} + +function addIntervalToDate(date: Date, value: number, unit: TimeUnit): Date { + const result = new Date(date); + switch (unit) { + case 'seconds': + result.setSeconds(result.getSeconds() + value); + break; + case 'minutes': + result.setMinutes(result.getMinutes() + value); + break; + case 'hours': + result.setHours(result.getHours() + value); + break; + case 'days': + result.setDate(result.getDate() + value); + break; + case 'weeks': + result.setDate(result.getDate() + value * 7); + break; + case 'months': + result.setMonth(result.getMonth() + value); + break; + default: + break; + } + return result; +} + +/** Checks a user's balance record and handles auto-refill if needed. */ +async function checkBalanceRecord( + txData: TxData, + deps: CheckBalanceDeps, +): Promise<{ canSpend: boolean; balance: number; tokenCost: number }> { + const { user, model, endpoint, valueKey, tokenType, amount, endpointTokenConfig } = txData; + const multiplier = deps.getMultiplier({ + valueKey, + tokenType, + model, + endpoint, + endpointTokenConfig, + }); + const tokenCost = amount * multiplier; + + const record = await deps.findBalanceByUser(user); + if (!record) { + logger.debug('[Balance.check] No balance record found for user', { user }); + return { canSpend: false, balance: 0, tokenCost }; + } + let balance = record.tokenCredits; + + logger.debug('[Balance.check] Initial state', { + user, + model, + endpoint, + valueKey, + tokenType, + amount, + balance, + multiplier, + endpointTokenConfig: !!endpointTokenConfig, + }); + + if ( + balance - tokenCost <= 0 && + record.autoRefillEnabled && + record.refillAmount && + record.refillAmount > 0 + ) { + const lastRefillDate = new Date(record.lastRefill ?? 0); + const now = new Date(); + if ( + isNaN(lastRefillDate.getTime()) || + now >= + addIntervalToDate( + lastRefillDate, + record.refillIntervalValue ?? 0, + record.refillIntervalUnit ?? 'days', + ) + ) { + try { + const result = await deps.createAutoRefillTransaction({ + user, + tokenType: 'credits', + context: 'autoRefill', + rawAmount: record.refillAmount, + }); + if (result) { + balance = result.balance; + } + } catch (error) { + logger.error('[Balance.check] Failed to record transaction for auto-refill', error); + } + } + } + + logger.debug('[Balance.check] Token cost', { tokenCost }); + return { canSpend: balance >= tokenCost, balance, tokenCost }; +} + +/** + * Checks balance for a user and logs a violation if they cannot spend. + * Throws an error with the balance info if insufficient funds. + */ +export async function checkBalance( + { req, res, txData }: { req: ServerRequest; res: Response; txData: TxData }, + deps: CheckBalanceDeps, +): Promise { + const { canSpend, balance, tokenCost } = await checkBalanceRecord(txData, deps); + if (canSpend) { + return true; + } + + const type = ViolationTypes.TOKEN_BALANCE; + const errorMessage: Record = { + type, + balance, + tokenCost, + promptTokens: txData.amount, + }; + + if (txData.generations && txData.generations.length > 0) { + errorMessage.generations = txData.generations; + } + + await deps.logViolation(req, res, type, errorMessage, 0); + throw new Error(JSON.stringify(errorMessage)); +} diff --git a/packages/api/src/middleware/index.ts b/packages/api/src/middleware/index.ts index 1f0cbc16fb..7787d89dfe 100644 --- a/packages/api/src/middleware/index.ts +++ b/packages/api/src/middleware/index.ts @@ -5,3 +5,4 @@ export * from './notFound'; export * from './balance'; export * from './json'; export * from './concurrency'; +export * from './checkBalance'; diff --git a/packages/api/src/prompts/format.ts b/packages/api/src/prompts/format.ts index df2b11b59a..de3d4e8a74 100644 --- a/packages/api/src/prompts/format.ts +++ b/packages/api/src/prompts/format.ts @@ -1,8 +1,8 @@ +import { escapeRegExp } from '@librechat/data-schemas'; import { SystemCategories } from 'librechat-data-provider'; import type { IPromptGroupDocument as IPromptGroup } from '@librechat/data-schemas'; import type { Types } from 'mongoose'; import type { PromptGroupsListResponse } from '~/types'; -import { escapeRegExp } from '~/utils/common'; /** * Formats prompt groups for the paginated /groups endpoint response diff --git a/packages/api/src/utils/common.ts b/packages/api/src/utils/common.ts index 6f4871b741..a5860b0a69 100644 --- a/packages/api/src/utils/common.ts +++ b/packages/api/src/utils/common.ts @@ -48,12 +48,3 @@ export function optionalChainWithEmptyCheck( } return values[values.length - 1]; } - -/** - * Escapes special characters in a string for use in a regular expression. - * @param str - The string to escape. - * @returns The escaped string safe for use in RegExp. - */ -export function escapeRegExp(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 3320fef949..50582832c0 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -20,7 +20,6 @@ export * from './openid'; export * from './promise'; export * from './ports'; export * from './sanitizeTitle'; -export * from './tempChatRetention'; export * from './text'; export * from './yaml'; export * from './http'; diff --git a/packages/data-schemas/rollup.config.js b/packages/data-schemas/rollup.config.js index c9f8838e77..d58331feee 100644 --- a/packages/data-schemas/rollup.config.js +++ b/packages/data-schemas/rollup.config.js @@ -29,7 +29,7 @@ export default { commonjs(), // Compile TypeScript files and generate type declarations typescript({ - tsconfig: './tsconfig.json', + tsconfig: './tsconfig.build.json', declaration: true, declarationDir: 'dist/types', rootDir: 'src', diff --git a/packages/data-schemas/src/index.ts b/packages/data-schemas/src/index.ts index a9c9a56078..ae69fc58bb 100644 --- a/packages/data-schemas/src/index.ts +++ b/packages/data-schemas/src/index.ts @@ -4,7 +4,15 @@ export * from './crypto'; export * from './schema'; export * from './utils'; export { createModels } from './models'; -export { createMethods, DEFAULT_REFRESH_TOKEN_EXPIRY, DEFAULT_SESSION_EXPIRY } from './methods'; +export { + createMethods, + DEFAULT_REFRESH_TOKEN_EXPIRY, + DEFAULT_SESSION_EXPIRY, + tokenValues, + cacheTokenValues, + premiumTokenValues, + defaultRate, +} from './methods'; export type * from './types'; export type * from './methods'; export { default as logger } from './config/winston'; diff --git a/packages/data-schemas/src/methods/aclEntry.ts b/packages/data-schemas/src/methods/aclEntry.ts index ff27a7046f..82e277254a 100644 --- a/packages/data-schemas/src/methods/aclEntry.ts +++ b/packages/data-schemas/src/methods/aclEntry.ts @@ -1,6 +1,12 @@ import { Types } from 'mongoose'; -import { PrincipalType, PrincipalModel } from 'librechat-data-provider'; -import type { Model, DeleteResult, ClientSession } from 'mongoose'; +import { PrincipalType, PrincipalModel, PermissionBits } from 'librechat-data-provider'; +import type { + AnyBulkWriteOperation, + ClientSession, + PipelineStage, + DeleteResult, + Model, +} from 'mongoose'; import type { IAclEntry } from '~/types'; export function createAclEntryMethods(mongoose: typeof import('mongoose')) { @@ -349,6 +355,103 @@ export function createAclEntryMethods(mongoose: typeof import('mongoose')) { return entries; } + /** + * Deletes ACL entries matching the given filter. + * @param filter - MongoDB filter query + * @param options - Optional query options (e.g., { session }) + */ + async function deleteAclEntries( + filter: Record, + options?: { session?: ClientSession }, + ): Promise { + const AclEntry = mongoose.models.AclEntry as Model; + return AclEntry.deleteMany(filter, options || {}); + } + + /** + * Performs a bulk write operation on ACL entries. + * @param ops - Array of bulk write operations + * @param options - Optional query options (e.g., { session }) + */ + async function bulkWriteAclEntries( + ops: AnyBulkWriteOperation[], + options?: { session?: ClientSession }, + ) { + const AclEntry = mongoose.models.AclEntry as Model; + return AclEntry.bulkWrite(ops, options || {}); + } + + /** + * Finds all publicly accessible resource IDs for a given resource type. + * @param resourceType - The type of resource + * @param requiredPermissions - Required permission bits + */ + async function findPublicResourceIds( + resourceType: string, + requiredPermissions: number, + ): Promise { + const AclEntry = mongoose.models.AclEntry as Model; + return AclEntry.find({ + principalType: PrincipalType.PUBLIC, + resourceType, + permBits: { $bitsAllSet: requiredPermissions }, + }).distinct('resourceId'); + } + + /** + * Runs an aggregation pipeline on the AclEntry collection. + * @param pipeline - MongoDB aggregation pipeline stages + */ + async function aggregateAclEntries(pipeline: PipelineStage[]) { + const AclEntry = mongoose.models.AclEntry as Model; + return AclEntry.aggregate(pipeline); + } + + /** + * Returns resource IDs solely owned by the given user (no other principals + * hold DELETE on the same resource). Handles both single and array resource types. + */ + async function getSoleOwnedResourceIds( + userObjectId: Types.ObjectId, + resourceTypes: string | string[], + ): Promise { + const AclEntry = mongoose.models.AclEntry as Model; + const types = Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes]; + + const ownedEntries = await AclEntry.find({ + principalType: PrincipalType.USER, + principalId: userObjectId, + resourceType: { $in: types }, + permBits: { $bitsAllSet: PermissionBits.DELETE }, + }) + .select('resourceId') + .lean(); + + if (ownedEntries.length === 0) { + return []; + } + + const ownedIds = ownedEntries.map((e) => e.resourceId); + + const otherOwners = await AclEntry.aggregate([ + { + $match: { + resourceType: { $in: types }, + resourceId: { $in: ownedIds }, + permBits: { $bitsAllSet: PermissionBits.DELETE }, + $or: [ + { principalId: { $ne: userObjectId } }, + { principalType: { $ne: PrincipalType.USER } }, + ], + }, + }, + { $group: { _id: '$resourceId' } }, + ]); + + const multiOwnerIds = new Set(otherOwners.map((doc: { _id: Types.ObjectId }) => doc._id.toString())); + return ownedIds.filter((id) => !multiOwnerIds.has(id.toString())); + } + return { findEntriesByPrincipal, findEntriesByResource, @@ -360,6 +463,11 @@ export function createAclEntryMethods(mongoose: typeof import('mongoose')) { revokePermission, modifyPermissionBits, findAccessibleResources, + deleteAclEntries, + bulkWriteAclEntries, + findPublicResourceIds, + aggregateAclEntries, + getSoleOwnedResourceIds, }; } diff --git a/packages/data-schemas/src/methods/action.ts b/packages/data-schemas/src/methods/action.ts new file mode 100644 index 0000000000..9467ad6a76 --- /dev/null +++ b/packages/data-schemas/src/methods/action.ts @@ -0,0 +1,77 @@ +import type { FilterQuery, Model } from 'mongoose'; +import type { IAction } from '~/types'; + +const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'] as const; + +export function createActionMethods(mongoose: typeof import('mongoose')) { + /** + * Update an action with new data without overwriting existing properties, + * or create a new action if it doesn't exist. + */ + async function updateAction( + searchParams: FilterQuery, + updateData: Partial, + ): Promise { + const Action = mongoose.models.Action as Model; + const options = { new: true, upsert: true }; + return (await Action.findOneAndUpdate( + searchParams, + updateData, + options, + ).lean()) as IAction | null; + } + + /** + * Retrieves all actions that match the given search parameters. + */ + async function getActions( + searchParams: FilterQuery, + includeSensitive = false, + ): Promise { + const Action = mongoose.models.Action as Model; + const actions = (await Action.find(searchParams).lean()) as IAction[]; + + if (!includeSensitive) { + for (let i = 0; i < actions.length; i++) { + const metadata = actions[i].metadata; + if (!metadata) { + continue; + } + + for (const field of sensitiveFields) { + if (metadata[field]) { + delete metadata[field]; + } + } + } + } + + return actions; + } + + /** + * Deletes an action by params. + */ + async function deleteAction(searchParams: FilterQuery): Promise { + const Action = mongoose.models.Action as Model; + return (await Action.findOneAndDelete(searchParams).lean()) as IAction | null; + } + + /** + * Deletes actions by params. + */ + async function deleteActions(searchParams: FilterQuery): Promise { + const Action = mongoose.models.Action as Model; + const result = await Action.deleteMany(searchParams); + return result.deletedCount; + } + + return { + getActions, + updateAction, + deleteAction, + deleteActions, + }; +} + +export type ActionMethods = ReturnType; diff --git a/api/models/Agent.spec.js b/packages/data-schemas/src/methods/agent.spec.ts similarity index 71% rename from api/models/Agent.spec.js rename to packages/data-schemas/src/methods/agent.spec.ts index ba2991cff7..f828c8c325 100644 --- a/api/models/Agent.spec.js +++ b/packages/data-schemas/src/methods/agent.spec.ts @@ -1,66 +1,120 @@ -const originalEnv = { - CREDS_KEY: process.env.CREDS_KEY, - CREDS_IV: process.env.CREDS_IV, +import mongoose from 'mongoose'; +import { v4 as uuidv4 } from 'uuid'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + AccessRoleIds, + ResourceType, + PrincipalType, + PrincipalModel, + PermissionBits, + EToolResources, +} from 'librechat-data-provider'; +import type { + UpdateWithAggregationPipeline, + RootFilterQuery, + QueryOptions, + UpdateQuery, +} from 'mongoose'; +import type { IAgent, IAclEntry, IUser, IAccessRole } from '..'; +import { createAgentMethods, type AgentMethods } from './agent'; +import { createModels } from '~/models'; + +/** Version snapshot stored in `IAgent.versions[]`. Extends the base omit with runtime-only fields. */ +type VersionEntry = Omit & { + __v?: number; + versions?: unknown; + version?: number; + updatedBy?: mongoose.Types.ObjectId; }; -process.env.CREDS_KEY = '0123456789abcdef0123456789abcdef'; -process.env.CREDS_IV = '0123456789abcdef'; - -jest.mock('~/server/services/Config', () => ({ - getCachedTools: jest.fn(), - getMCPServerTools: jest.fn(), +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), })); -const mongoose = require('mongoose'); -const { v4: uuidv4 } = require('uuid'); -const { agentSchema } = require('@librechat/data-schemas'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { - ResourceType, - AccessRoleIds, - PrincipalType, - PermissionBits, -} = require('librechat-data-provider'); -const { - getAgent, - loadAgent, - createAgent, - updateAgent, - deleteAgent, - deleteUserAgents, - revertAgentVersion, - addAgentResourceFile, - getListAgentsByAccess, - removeAgentResourceFiles, - generateActionMetadataHash, -} = require('./Agent'); -const permissionService = require('~/server/services/PermissionService'); -const { getCachedTools, getMCPServerTools } = require('~/server/services/Config'); -const { AclEntry, User } = require('~/db/models'); +let mongoServer: InstanceType; +let Agent: mongoose.Model; +let AclEntry: mongoose.Model; +let User: mongoose.Model; +let AccessRole: mongoose.Model; +let modelsToCleanup: string[] = []; +let methods: ReturnType; -/** - * @type {import('mongoose').Model} - */ -let Agent; +let createAgent: AgentMethods['createAgent']; +let getAgent: AgentMethods['getAgent']; +let updateAgent: AgentMethods['updateAgent']; +let deleteAgent: AgentMethods['deleteAgent']; +let deleteUserAgents: AgentMethods['deleteUserAgents']; +let revertAgentVersion: AgentMethods['revertAgentVersion']; +let addAgentResourceFile: AgentMethods['addAgentResourceFile']; +let removeAgentResourceFiles: AgentMethods['removeAgentResourceFiles']; +let getListAgentsByAccess: AgentMethods['getListAgentsByAccess']; +let generateActionMetadataHash: AgentMethods['generateActionMetadataHash']; -describe('models/Agent', () => { +const getActions = jest.fn().mockResolvedValue([]); + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + + const models = createModels(mongoose); + modelsToCleanup = Object.keys(models); + Agent = mongoose.models.Agent as mongoose.Model; + AclEntry = mongoose.models.AclEntry as mongoose.Model; + User = mongoose.models.User as mongoose.Model; + AccessRole = mongoose.models.AccessRole as mongoose.Model; + + const removeAllPermissions = async ({ + resourceType, + resourceId, + }: { + resourceType: string; + resourceId: unknown; + }) => { + await AclEntry.deleteMany({ resourceType, resourceId }); + }; + + methods = createAgentMethods(mongoose, { removeAllPermissions, getActions }); + createAgent = methods.createAgent; + getAgent = methods.getAgent; + updateAgent = methods.updateAgent; + deleteAgent = methods.deleteAgent; + deleteUserAgents = methods.deleteUserAgents; + revertAgentVersion = methods.revertAgentVersion; + addAgentResourceFile = methods.addAgentResourceFile; + removeAgentResourceFiles = methods.removeAgentResourceFiles; + getListAgentsByAccess = methods.getListAgentsByAccess; + generateActionMetadataHash = methods.generateActionMetadataHash; + + await mongoose.connect(mongoUri); + + await AccessRole.create({ + accessRoleId: AccessRoleIds.AGENT_OWNER, + name: 'Owner', + description: 'Full control over agents', + resourceType: ResourceType.AGENT, + permBits: 15, + }); +}, 30000); + +afterAll(async () => { + const collections = mongoose.connection.collections; + for (const key in collections) { + await collections[key].deleteMany({}); + } + for (const modelName of modelsToCleanup) { + if (mongoose.models[modelName]) { + delete (mongoose.models as Record)[modelName]; + } + } + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +describe('Agent Methods', () => { describe('Agent Resource File Operations', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - process.env.CREDS_KEY = originalEnv.CREDS_KEY; - process.env.CREDS_IV = originalEnv.CREDS_IV; - }); - beforeEach(async () => { await Agent.deleteMany({}); await User.deleteMany({}); @@ -77,10 +131,10 @@ describe('models/Agent', () => { file_id: fileId, }); - expect(updatedAgent.tools).toContain(toolResource); - expect(Array.isArray(updatedAgent.tools)).toBe(true); + expect(updatedAgent!.tools).toContain(toolResource); + expect(Array.isArray(updatedAgent!.tools)).toBe(true); // Should not duplicate - const count = updatedAgent.tools.filter((t) => t === toolResource).length; + const count = updatedAgent!.tools?.filter((t) => t === toolResource).length ?? 0; expect(count).toBe(1); }); @@ -104,9 +158,9 @@ describe('models/Agent', () => { file_id: fileId2, }); - expect(updatedAgent.tools).toContain(toolResource); - expect(Array.isArray(updatedAgent.tools)).toBe(true); - const count = updatedAgent.tools.filter((t) => t === toolResource).length; + expect(updatedAgent!.tools).toContain(toolResource); + expect(Array.isArray(updatedAgent!.tools)).toBe(true); + const count = updatedAgent!.tools?.filter((t) => t === toolResource).length ?? 0; expect(count).toBe(1); }); @@ -120,9 +174,13 @@ describe('models/Agent', () => { await Promise.all(additionPromises); const updatedAgent = await Agent.findOne({ id: agent.id }); - expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); - expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(10); - expect(new Set(updatedAgent.tool_resources.test_tool.file_ids).size).toBe(10); + expect(updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids).toBeDefined(); + expect(updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids).toHaveLength( + 10, + ); + expect( + new Set(updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids).size, + ).toBe(10); }); test('should handle concurrent additions and removals', async () => { @@ -132,18 +190,18 @@ describe('models/Agent', () => { await Promise.all(createFileOperations(agent.id, initialFileIds, 'add')); const newFileIds = Array.from({ length: 5 }, () => uuidv4()); - const operations = [ + const operations: Promise[] = [ ...newFileIds.map((fileId) => addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'test_tool', + tool_resource: EToolResources.execute_code, file_id: fileId, }), ), ...initialFileIds.map((fileId) => removeAgentResourceFiles({ agent_id: agent.id, - files: [{ tool_resource: 'test_tool', file_id: fileId }], + files: [{ tool_resource: EToolResources.execute_code, file_id: fileId }], }), ), ]; @@ -151,8 +209,8 @@ describe('models/Agent', () => { await Promise.all(operations); const updatedAgent = await Agent.findOne({ id: agent.id }); - expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); - expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(5); + expect(updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids).toBeDefined(); + expect(updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids).toHaveLength(5); }); test('should initialize array when adding to non-existent tool resource', async () => { @@ -161,13 +219,13 @@ describe('models/Agent', () => { const updatedAgent = await addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'new_tool', + tool_resource: EToolResources.context, file_id: fileId, }); - expect(updatedAgent.tool_resources.new_tool.file_ids).toBeDefined(); - expect(updatedAgent.tool_resources.new_tool.file_ids).toHaveLength(1); - expect(updatedAgent.tool_resources.new_tool.file_ids[0]).toBe(fileId); + expect(updatedAgent?.tool_resources?.[EToolResources.context]?.file_ids).toBeDefined(); + expect(updatedAgent?.tool_resources?.[EToolResources.context]?.file_ids).toHaveLength(1); + expect(updatedAgent?.tool_resources?.[EToolResources.context]?.file_ids?.[0]).toBe(fileId); }); test('should handle rapid sequential modifications to same tool resource', async () => { @@ -177,27 +235,33 @@ describe('models/Agent', () => { for (let i = 0; i < 10; i++) { await addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'test_tool', + tool_resource: EToolResources.execute_code, file_id: `${fileId}_${i}`, }); if (i % 2 === 0) { await removeAgentResourceFiles({ agent_id: agent.id, - files: [{ tool_resource: 'test_tool', file_id: `${fileId}_${i}` }], + files: [{ tool_resource: EToolResources.execute_code, file_id: `${fileId}_${i}` }], }); } } const updatedAgent = await Agent.findOne({ id: agent.id }); - expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); - expect(Array.isArray(updatedAgent.tool_resources.test_tool.file_ids)).toBe(true); + expect(updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids).toBeDefined(); + expect( + Array.isArray(updatedAgent!.tool_resources![EToolResources.execute_code]!.file_ids), + ).toBe(true); }); test('should handle multiple tool resources concurrently', async () => { const agent = await createBasicAgent(); - const toolResources = ['tool1', 'tool2', 'tool3']; - const operations = []; + const toolResources = [ + EToolResources.file_search, + EToolResources.execute_code, + EToolResources.image_edit, + ] as const; + const operations: Promise[] = []; toolResources.forEach((tool) => { const fileIds = Array.from({ length: 5 }, () => uuidv4()); @@ -216,8 +280,8 @@ describe('models/Agent', () => { const updatedAgent = await Agent.findOne({ id: agent.id }); toolResources.forEach((tool) => { - expect(updatedAgent.tool_resources[tool].file_ids).toBeDefined(); - expect(updatedAgent.tool_resources[tool].file_ids).toHaveLength(5); + expect(updatedAgent!.tool_resources![tool]!.file_ids).toBeDefined(); + expect(updatedAgent!.tool_resources![tool]!.file_ids).toHaveLength(5); }); }); @@ -246,7 +310,7 @@ describe('models/Agent', () => { if (setupFile) { await addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'test_tool', + tool_resource: EToolResources.execute_code, file_id: fileId, }); } @@ -255,19 +319,19 @@ describe('models/Agent', () => { operation === 'add' ? addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'test_tool', + tool_resource: EToolResources.execute_code, file_id: fileId, }) : removeAgentResourceFiles({ agent_id: agent.id, - files: [{ tool_resource: 'test_tool', file_id: fileId }], + files: [{ tool_resource: EToolResources.execute_code, file_id: fileId }], }), ); await Promise.all(promises); const updatedAgent = await Agent.findOne({ id: agent.id }); - const fileIds = updatedAgent.tool_resources?.test_tool?.file_ids ?? []; + const fileIds = updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids ?? []; expect(fileIds).toHaveLength(expectedLength); if (expectedContains) { @@ -284,27 +348,27 @@ describe('models/Agent', () => { await addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'test_tool', + tool_resource: EToolResources.execute_code, file_id: fileId, }); - const operations = [ + const operations: Promise[] = [ addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'test_tool', + tool_resource: EToolResources.execute_code, file_id: fileId, }), removeAgentResourceFiles({ agent_id: agent.id, - files: [{ tool_resource: 'test_tool', file_id: fileId }], + files: [{ tool_resource: EToolResources.execute_code, file_id: fileId }], }), ]; await Promise.all(operations); const updatedAgent = await Agent.findOne({ id: agent.id }); - const finalFileIds = updatedAgent.tool_resources.test_tool.file_ids; - const count = finalFileIds.filter((id) => id === fileId).length; + const finalFileIds = updatedAgent!.tool_resources![EToolResources.execute_code]!.file_ids!; + const count = finalFileIds.filter((id: string) => id === fileId).length; expect(count).toBeLessThanOrEqual(1); if (count === 0) { @@ -324,7 +388,7 @@ describe('models/Agent', () => { fileIds.map((fileId) => addAgentResourceFile({ agent_id: agent.id, - tool_resource: 'test_tool', + tool_resource: EToolResources.execute_code, file_id: fileId, }), ), @@ -334,7 +398,7 @@ describe('models/Agent', () => { const removalPromises = fileIds.map((fileId) => removeAgentResourceFiles({ agent_id: agent.id, - files: [{ tool_resource: 'test_tool', file_id: fileId }], + files: [{ tool_resource: EToolResources.execute_code, file_id: fileId }], }), ); @@ -342,7 +406,8 @@ describe('models/Agent', () => { const updatedAgent = await Agent.findOne({ id: agent.id }); // Check if the array is empty or the tool resource itself is removed - const finalFileIds = updatedAgent.tool_resources?.test_tool?.file_ids ?? []; + const finalFileIds = + updatedAgent?.tool_resources?.[EToolResources.execute_code]?.file_ids ?? []; expect(finalFileIds).toHaveLength(0); }); @@ -366,7 +431,7 @@ describe('models/Agent', () => { ])('addAgentResourceFile with $name', ({ needsAgent, params, shouldResolve, error }) => { test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { const agent = needsAgent ? await createBasicAgent() : null; - const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; + const agent_id = needsAgent ? agent!.id : `agent_${uuidv4()}`; if (shouldResolve) { await expect(addAgentResourceFile({ agent_id, ...params })).resolves.toBeDefined(); @@ -379,7 +444,7 @@ describe('models/Agent', () => { describe.each([ { name: 'empty files array', - files: [], + files: [] as { tool_resource: string; file_id: string }[], needsAgent: true, shouldResolve: true, }, @@ -399,7 +464,7 @@ describe('models/Agent', () => { ])('removeAgentResourceFiles with $name', ({ files, needsAgent, shouldResolve, error }) => { test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { const agent = needsAgent ? await createBasicAgent() : null; - const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; + const agent_id = needsAgent ? agent!.id : `agent_${uuidv4()}`; if (shouldResolve) { const result = await removeAgentResourceFiles({ agent_id, files }); @@ -416,36 +481,9 @@ describe('models/Agent', () => { }); describe('Agent CRUD Operations', () => { - let mongoServer; - let AccessRole; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - - // Initialize models - const dbModels = require('~/db/models'); - AccessRole = dbModels.AccessRole; - - // Create necessary access roles for agents - await AccessRole.create({ - accessRoleId: AccessRoleIds.AGENT_OWNER, - name: 'Owner', - description: 'Full control over agents', - resourceType: ResourceType.AGENT, - permBits: 15, // VIEW | EDIT | DELETE | SHARE - }); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); - beforeEach(async () => { await Agent.deleteMany({}); + await User.deleteMany({}); await AclEntry.deleteMany({}); }); @@ -467,9 +505,9 @@ describe('models/Agent', () => { const retrievedAgent = await getAgent({ id: agentId }); expect(retrievedAgent).toBeDefined(); - expect(retrievedAgent.id).toBe(agentId); - expect(retrievedAgent.name).toBe('Test Agent'); - expect(retrievedAgent.description).toBe('Test description'); + expect(retrievedAgent!.id).toBe(agentId); + expect(retrievedAgent!.name).toBe('Test Agent'); + expect(retrievedAgent!.description).toBe('Test description'); }); test('should delete an agent', async () => { @@ -507,8 +545,9 @@ describe('models/Agent', () => { }); // Grant permissions (simulating sharing) - await permissionService.grantPermission({ + await AclEntry.create({ principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, principalId: authorId, resourceType: ResourceType.AGENT, resourceId: agent._id, @@ -570,15 +609,15 @@ describe('models/Agent', () => { // Verify edge exists before deletion const sourceAgentBefore = await getAgent({ id: sourceAgentId }); - expect(sourceAgentBefore.edges).toHaveLength(1); - expect(sourceAgentBefore.edges[0].to).toBe(targetAgentId); + expect(sourceAgentBefore!.edges).toHaveLength(1); + expect(sourceAgentBefore!.edges![0].to).toBe(targetAgentId); // Delete the target agent await deleteAgent({ id: targetAgentId }); // Verify the edge is removed from source agent const sourceAgentAfter = await getAgent({ id: sourceAgentId }); - expect(sourceAgentAfter.edges).toHaveLength(0); + expect(sourceAgentAfter!.edges).toHaveLength(0); }); test('should remove agent from user favorites when agent is deleted', async () => { @@ -606,8 +645,10 @@ describe('models/Agent', () => { // Verify user has agent in favorites const userBefore = await User.findById(userId); - expect(userBefore.favorites).toHaveLength(2); - expect(userBefore.favorites.some((f) => f.agentId === agentId)).toBe(true); + expect(userBefore!.favorites).toHaveLength(2); + expect( + userBefore!.favorites!.some((f: Record) => f.agentId === agentId), + ).toBe(true); // Delete the agent await deleteAgent({ id: agentId }); @@ -618,9 +659,13 @@ describe('models/Agent', () => { // Verify agent is removed from user favorites const userAfter = await User.findById(userId); - expect(userAfter.favorites).toHaveLength(1); - expect(userAfter.favorites.some((f) => f.agentId === agentId)).toBe(false); - expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true); + expect(userAfter!.favorites).toHaveLength(1); + expect( + userAfter!.favorites!.some((f: Record) => f.agentId === agentId), + ).toBe(false); + expect(userAfter!.favorites!.some((f: Record) => f.model === 'gpt-4')).toBe( + true, + ); }); test('should remove agent from multiple users favorites when agent is deleted', async () => { @@ -662,9 +707,11 @@ describe('models/Agent', () => { const user1After = await User.findById(user1Id); const user2After = await User.findById(user2Id); - expect(user1After.favorites).toHaveLength(0); - expect(user2After.favorites).toHaveLength(1); - expect(user2After.favorites.some((f) => f.agentId === agentId)).toBe(false); + expect(user1After!.favorites).toHaveLength(0); + expect(user2After!.favorites).toHaveLength(1); + expect( + user2After!.favorites!.some((f: Record) => f.agentId === agentId), + ).toBe(false); }); test('should preserve other agents in database when one agent is deleted', async () => { @@ -711,9 +758,9 @@ describe('models/Agent', () => { const keptAgent1 = await getAgent({ id: agentToKeep1Id }); const keptAgent2 = await getAgent({ id: agentToKeep2Id }); expect(keptAgent1).not.toBeNull(); - expect(keptAgent1.name).toBe('Agent To Keep 1'); + expect(keptAgent1!.name).toBe('Agent To Keep 1'); expect(keptAgent2).not.toBeNull(); - expect(keptAgent2.name).toBe('Agent To Keep 2'); + expect(keptAgent2!.name).toBe('Agent To Keep 2'); }); test('should preserve other agents in user favorites when one agent is deleted', async () => { @@ -763,17 +810,23 @@ describe('models/Agent', () => { // Verify user has all three agents in favorites const userBefore = await User.findById(userId); - expect(userBefore.favorites).toHaveLength(3); + expect(userBefore!.favorites).toHaveLength(3); // Delete one agent await deleteAgent({ id: agentToDeleteId }); // Verify only the deleted agent is removed from favorites const userAfter = await User.findById(userId); - expect(userAfter.favorites).toHaveLength(2); - expect(userAfter.favorites.some((f) => f.agentId === agentToDeleteId)).toBe(false); - expect(userAfter.favorites.some((f) => f.agentId === agentToKeep1Id)).toBe(true); - expect(userAfter.favorites.some((f) => f.agentId === agentToKeep2Id)).toBe(true); + expect(userAfter!.favorites).toHaveLength(2); + expect( + userAfter!.favorites?.some((f: Record) => f.agentId === agentToDeleteId), + ).toBe(false); + expect( + userAfter!.favorites?.some((f: Record) => f.agentId === agentToKeep1Id), + ).toBe(true); + expect( + userAfter!.favorites?.some((f: Record) => f.agentId === agentToKeep2Id), + ).toBe(true); }); test('should not affect users who do not have deleted agent in favorites', async () => { @@ -823,15 +876,27 @@ describe('models/Agent', () => { // Verify user with deleted agent has it removed const userWithDeleted = await User.findById(userWithDeletedAgentId); - expect(userWithDeleted.favorites).toHaveLength(1); - expect(userWithDeleted.favorites.some((f) => f.agentId === agentToDeleteId)).toBe(false); - expect(userWithDeleted.favorites.some((f) => f.model === 'gpt-4')).toBe(true); + expect(userWithDeleted!.favorites).toHaveLength(1); + expect( + userWithDeleted!.favorites!.some( + (f: Record) => f.agentId === agentToDeleteId, + ), + ).toBe(false); + expect( + userWithDeleted!.favorites!.some((f: Record) => f.model === 'gpt-4'), + ).toBe(true); // Verify user without deleted agent is completely unaffected const userWithoutDeleted = await User.findById(userWithoutDeletedAgentId); - expect(userWithoutDeleted.favorites).toHaveLength(2); - expect(userWithoutDeleted.favorites.some((f) => f.agentId === otherAgentId)).toBe(true); - expect(userWithoutDeleted.favorites.some((f) => f.model === 'claude-3')).toBe(true); + expect(userWithoutDeleted!.favorites).toHaveLength(2); + expect( + userWithoutDeleted!.favorites!.some( + (f: Record) => f.agentId === otherAgentId, + ), + ).toBe(true); + expect( + userWithoutDeleted!.favorites!.some((f: Record) => f.model === 'claude-3'), + ).toBe(true); }); test('should remove all user agents from favorites when deleteUserAgents is called', async () => { @@ -898,7 +963,7 @@ describe('models/Agent', () => { }); const userBefore = await User.findById(userId); - expect(userBefore.favorites).toHaveLength(4); + expect(userBefore!.favorites).toHaveLength(4); await deleteUserAgents(authorId.toString()); @@ -908,11 +973,21 @@ describe('models/Agent', () => { expect(await getAgent({ id: otherAuthorAgentId })).not.toBeNull(); const userAfter = await User.findById(userId); - expect(userAfter.favorites).toHaveLength(2); - expect(userAfter.favorites.some((f) => f.agentId === agent1Id)).toBe(false); - expect(userAfter.favorites.some((f) => f.agentId === agent2Id)).toBe(false); - expect(userAfter.favorites.some((f) => f.agentId === otherAuthorAgentId)).toBe(true); - expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true); + expect(userAfter!.favorites).toHaveLength(2); + expect( + userAfter!.favorites!.some((f: Record) => f.agentId === agent1Id), + ).toBe(false); + expect( + userAfter!.favorites!.some((f: Record) => f.agentId === agent2Id), + ).toBe(false); + expect( + userAfter!.favorites!.some( + (f: Record) => f.agentId === otherAuthorAgentId, + ), + ).toBe(true); + expect(userAfter!.favorites!.some((f: Record) => f.model === 'gpt-4')).toBe( + true, + ); }); test('should handle deleteUserAgents when agents are in multiple users favorites', async () => { @@ -985,17 +1060,25 @@ describe('models/Agent', () => { await deleteUserAgents(authorId.toString()); const user1After = await User.findById(user1Id); - expect(user1After.favorites).toHaveLength(0); + expect(user1After!.favorites).toHaveLength(0); const user2After = await User.findById(user2Id); - expect(user2After.favorites).toHaveLength(1); - expect(user2After.favorites.some((f) => f.agentId === agent1Id)).toBe(false); - expect(user2After.favorites.some((f) => f.model === 'claude-3')).toBe(true); + expect(user2After!.favorites).toHaveLength(1); + expect( + user2After!.favorites!.some((f: Record) => f.agentId === agent1Id), + ).toBe(false); + expect( + user2After!.favorites!.some((f: Record) => f.model === 'claude-3'), + ).toBe(true); const user3After = await User.findById(user3Id); - expect(user3After.favorites).toHaveLength(2); - expect(user3After.favorites.some((f) => f.agentId === unrelatedAgentId)).toBe(true); - expect(user3After.favorites.some((f) => f.model === 'gpt-4')).toBe(true); + expect(user3After!.favorites).toHaveLength(2); + expect( + user3After!.favorites!.some((f: Record) => f.agentId === unrelatedAgentId), + ).toBe(true); + expect(user3After!.favorites!.some((f: Record) => f.model === 'gpt-4')).toBe( + true, + ); }); test('should handle deleteUserAgents when user has no agents', async () => { @@ -1035,9 +1118,13 @@ describe('models/Agent', () => { expect(await getAgent({ id: existingAgentId })).not.toBeNull(); const userAfter = await User.findById(userId); - expect(userAfter.favorites).toHaveLength(2); - expect(userAfter.favorites.some((f) => f.agentId === existingAgentId)).toBe(true); - expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true); + expect(userAfter!.favorites).toHaveLength(2); + expect( + userAfter!.favorites!.some((f: Record) => f.agentId === existingAgentId), + ).toBe(true); + expect(userAfter!.favorites!.some((f: Record) => f.model === 'gpt-4')).toBe( + true, + ); }); test('should handle deleteUserAgents when agents are not in any favorites', async () => { @@ -1097,8 +1184,10 @@ describe('models/Agent', () => { expect(await getAgent({ id: agent2Id })).toBeNull(); const userAfter = await User.findById(userId); - expect(userAfter.favorites).toHaveLength(1); - expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true); + expect(userAfter!.favorites).toHaveLength(1); + expect(userAfter!.favorites!.some((f: Record) => f.model === 'gpt-4')).toBe( + true, + ); }); test('should preserve multi-owned agents when deleteUserAgents is called', async () => { @@ -1124,30 +1213,27 @@ describe('models/Agent', () => { author: deletingUserId, }); - await permissionService.grantPermission({ + await AclEntry.create({ principalType: PrincipalType.USER, principalId: deletingUserId, resourceType: ResourceType.AGENT, - resourceId: soleAgent._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, - grantedBy: deletingUserId, + resourceId: (soleAgent as unknown as { _id: mongoose.Types.ObjectId })._id, + permBits: PermissionBits.DELETE | PermissionBits.READ | PermissionBits.WRITE, }); - await permissionService.grantPermission({ + await AclEntry.create({ principalType: PrincipalType.USER, principalId: deletingUserId, resourceType: ResourceType.AGENT, - resourceId: multiAgent._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, - grantedBy: deletingUserId, + resourceId: (multiAgent as unknown as { _id: mongoose.Types.ObjectId })._id, + permBits: PermissionBits.DELETE | PermissionBits.READ | PermissionBits.WRITE, }); - await permissionService.grantPermission({ + await AclEntry.create({ principalType: PrincipalType.USER, principalId: otherOwnerId, resourceType: ResourceType.AGENT, - resourceId: multiAgent._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, - grantedBy: otherOwnerId, + resourceId: (multiAgent as unknown as { _id: mongoose.Types.ObjectId })._id, + permBits: PermissionBits.DELETE | PermissionBits.READ | PermissionBits.WRITE, }); await deleteUserAgents(deletingUserId.toString()); @@ -1157,13 +1243,13 @@ describe('models/Agent', () => { const soleAcl = await AclEntry.find({ resourceType: ResourceType.AGENT, - resourceId: soleAgent._id, + resourceId: (soleAgent as unknown as { _id: mongoose.Types.ObjectId })._id, }); expect(soleAcl).toHaveLength(0); const multiAcl = await AclEntry.find({ resourceType: ResourceType.AGENT, - resourceId: multiAgent._id, + resourceId: (multiAgent as unknown as { _id: mongoose.Types.ObjectId })._id, principalId: otherOwnerId, }); expect(multiAcl).toHaveLength(1); @@ -1171,7 +1257,7 @@ describe('models/Agent', () => { const deletingUserMultiAcl = await AclEntry.find({ resourceType: ResourceType.AGENT, - resourceId: multiAgent._id, + resourceId: (multiAgent as unknown as { _id: mongoose.Types.ObjectId })._id, principalId: deletingUserId, }); expect(deletingUserMultiAcl).toHaveLength(1); @@ -1194,66 +1280,11 @@ describe('models/Agent', () => { expect(await getAgent({ id: legacyAgentId })).toBeNull(); }); - test('should handle ephemeral agent loading', async () => { - const agentId = 'ephemeral_test'; - const endpoint = 'openai'; - - const originalModule = jest.requireActual('librechat-data-provider'); - - const mockDataProvider = { - ...originalModule, - Constants: { - ...originalModule.Constants, - EPHEMERAL_AGENT_ID: 'ephemeral_test', - }, - }; - - jest.doMock('librechat-data-provider', () => mockDataProvider); - - expect(agentId).toBeDefined(); - expect(endpoint).toBeDefined(); - - jest.dontMock('librechat-data-provider'); - }); - - test('should handle loadAgent functionality and errors', async () => { - const agentId = `agent_${uuidv4()}`; - const authorId = new mongoose.Types.ObjectId(); - - await createAgent({ - id: agentId, - name: 'Test Load Agent', - provider: 'test', - model: 'test-model', - author: authorId, - tools: ['tool1', 'tool2'], - }); - - const agent = await getAgent({ id: agentId }); - - expect(agent).toBeDefined(); - expect(agent.id).toBe(agentId); - expect(agent.name).toBe('Test Load Agent'); - expect(agent.tools).toEqual(expect.arrayContaining(['tool1', 'tool2'])); - - const mockLoadAgent = jest.fn().mockResolvedValue(agent); - const loadedAgent = await mockLoadAgent(); - expect(loadedAgent).toBeDefined(); - expect(loadedAgent.id).toBe(agentId); - - const nonExistentId = `agent_${uuidv4()}`; - const nonExistentAgent = await getAgent({ id: nonExistentId }); - expect(nonExistentAgent).toBeNull(); - - const mockLoadAgentError = jest.fn().mockRejectedValue(new Error('No agent found with ID')); - await expect(mockLoadAgentError()).rejects.toThrow('No agent found with ID'); - }); - describe('Edge Cases', () => { test.each([ { name: 'getAgent with undefined search parameters', - fn: () => getAgent(undefined), + fn: () => getAgent(undefined as unknown as Parameters[0]), expected: null, }, { @@ -1269,20 +1300,6 @@ describe('models/Agent', () => { }); describe('Agent Version History', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); - beforeEach(async () => { await Agent.deleteMany({}); }); @@ -1290,12 +1307,12 @@ describe('models/Agent', () => { test('should create an agent with a single entry in versions array', async () => { const agent = await createBasicAgent(); - expect(agent.versions).toBeDefined(); + expect(agent!.versions).toBeDefined(); expect(Array.isArray(agent.versions)).toBe(true); - expect(agent.versions).toHaveLength(1); - expect(agent.versions[0].name).toBe('Test Agent'); - expect(agent.versions[0].provider).toBe('test'); - expect(agent.versions[0].model).toBe('test-model'); + expect(agent!.versions).toHaveLength(1); + expect(agent!.versions![0].name).toBe('Test Agent'); + expect(agent!.versions![0].provider).toBe('test'); + expect(agent!.versions![0].model).toBe('test-model'); }); test('should accumulate version history across multiple updates', async () => { @@ -1317,29 +1334,29 @@ describe('models/Agent', () => { await updateAgent({ id: agentId }, { name: 'Third Name', model: 'new-model' }); const finalAgent = await updateAgent({ id: agentId }, { description: 'Final description' }); - expect(finalAgent.versions).toBeDefined(); - expect(Array.isArray(finalAgent.versions)).toBe(true); - expect(finalAgent.versions).toHaveLength(4); + expect(finalAgent!.versions).toBeDefined(); + expect(Array.isArray(finalAgent!.versions)).toBe(true); + expect(finalAgent!.versions).toHaveLength(4); - expect(finalAgent.versions[0].name).toBe('First Name'); - expect(finalAgent.versions[0].description).toBe('First description'); - expect(finalAgent.versions[0].model).toBe('test-model'); + expect(finalAgent!.versions![0].name).toBe('First Name'); + expect(finalAgent!.versions![0].description).toBe('First description'); + expect(finalAgent!.versions![0].model).toBe('test-model'); - expect(finalAgent.versions[1].name).toBe('Second Name'); - expect(finalAgent.versions[1].description).toBe('Second description'); - expect(finalAgent.versions[1].model).toBe('test-model'); + expect(finalAgent!.versions![1].name).toBe('Second Name'); + expect(finalAgent!.versions![1].description).toBe('Second description'); + expect(finalAgent!.versions![1].model).toBe('test-model'); - expect(finalAgent.versions[2].name).toBe('Third Name'); - expect(finalAgent.versions[2].description).toBe('Second description'); - expect(finalAgent.versions[2].model).toBe('new-model'); + expect(finalAgent!.versions![2].name).toBe('Third Name'); + expect(finalAgent!.versions![2].description).toBe('Second description'); + expect(finalAgent!.versions![2].model).toBe('new-model'); - expect(finalAgent.versions[3].name).toBe('Third Name'); - expect(finalAgent.versions[3].description).toBe('Final description'); - expect(finalAgent.versions[3].model).toBe('new-model'); + expect(finalAgent!.versions![3].name).toBe('Third Name'); + expect(finalAgent!.versions![3].description).toBe('Final description'); + expect(finalAgent!.versions![3].model).toBe('new-model'); - expect(finalAgent.name).toBe('Third Name'); - expect(finalAgent.description).toBe('Final description'); - expect(finalAgent.model).toBe('new-model'); + expect(finalAgent!.name).toBe('Third Name'); + expect(finalAgent!.description).toBe('Final description'); + expect(finalAgent!.model).toBe('new-model'); }); test('should not include metadata fields in version history', async () => { @@ -1354,14 +1371,14 @@ describe('models/Agent', () => { const updatedAgent = await updateAgent({ id: agentId }, { description: 'New description' }); - expect(updatedAgent.versions).toHaveLength(2); - expect(updatedAgent.versions[0]._id).toBeUndefined(); - expect(updatedAgent.versions[0].__v).toBeUndefined(); - expect(updatedAgent.versions[0].name).toBe('Test Agent'); - expect(updatedAgent.versions[0].author).toBeUndefined(); + expect(updatedAgent!.versions).toHaveLength(2); + expect(updatedAgent!.versions![0]._id).toBeUndefined(); + expect((updatedAgent!.versions![0] as VersionEntry).__v).toBeUndefined(); + expect(updatedAgent!.versions![0].name).toBe('Test Agent'); + expect(updatedAgent!.versions![0].author).toBeUndefined(); - expect(updatedAgent.versions[1]._id).toBeUndefined(); - expect(updatedAgent.versions[1].__v).toBeUndefined(); + expect(updatedAgent!.versions![1]._id).toBeUndefined(); + expect((updatedAgent!.versions![1] as VersionEntry).__v).toBeUndefined(); }); test('should not recursively include previous versions', async () => { @@ -1378,10 +1395,10 @@ describe('models/Agent', () => { await updateAgent({ id: agentId }, { name: 'Updated Name 2' }); const finalAgent = await updateAgent({ id: agentId }, { name: 'Updated Name 3' }); - expect(finalAgent.versions).toHaveLength(4); + expect(finalAgent!.versions).toHaveLength(4); - finalAgent.versions.forEach((version) => { - expect(version.versions).toBeUndefined(); + finalAgent!.versions!.forEach((version) => { + expect((version as VersionEntry).versions).toBeUndefined(); }); }); @@ -1407,10 +1424,10 @@ describe('models/Agent', () => { ); const firstUpdate = await getAgent({ id: agentId }); - expect(firstUpdate.description).toBe('Updated description'); - expect(firstUpdate.tools).toContain('tool1'); - expect(firstUpdate.tools).toContain('tool2'); - expect(firstUpdate.versions).toHaveLength(2); + expect(firstUpdate!.description).toBe('Updated description'); + expect(firstUpdate!.tools).toContain('tool1'); + expect(firstUpdate!.tools).toContain('tool2'); + expect(firstUpdate!.versions).toHaveLength(2); await updateAgent( { id: agentId }, @@ -1420,11 +1437,11 @@ describe('models/Agent', () => { ); const secondUpdate = await getAgent({ id: agentId }); - expect(secondUpdate.tools).toHaveLength(2); - expect(secondUpdate.tools).toContain('tool2'); - expect(secondUpdate.tools).toContain('tool3'); - expect(secondUpdate.tools).not.toContain('tool1'); - expect(secondUpdate.versions).toHaveLength(3); + expect(secondUpdate!.tools).toHaveLength(2); + expect(secondUpdate!.tools).toContain('tool2'); + expect(secondUpdate!.tools).toContain('tool3'); + expect(secondUpdate!.tools).not.toContain('tool1'); + expect(secondUpdate!.versions).toHaveLength(3); await updateAgent( { id: agentId }, @@ -1434,9 +1451,9 @@ describe('models/Agent', () => { ); const thirdUpdate = await getAgent({ id: agentId }); - const toolCount = thirdUpdate.tools.filter((t) => t === 'tool3').length; + const toolCount = thirdUpdate!.tools!.filter((t) => t === 'tool3').length; expect(toolCount).toBe(2); - expect(thirdUpdate.versions).toHaveLength(4); + expect(thirdUpdate!.versions).toHaveLength(4); }); test('should handle parameter objects correctly', async () => { @@ -1457,8 +1474,8 @@ describe('models/Agent', () => { { model_parameters: { temperature: 0.8 } }, ); - expect(updatedAgent.versions).toHaveLength(2); - expect(updatedAgent.model_parameters.temperature).toBe(0.8); + expect(updatedAgent!.versions).toHaveLength(2); + expect(updatedAgent!.model_parameters?.temperature).toBe(0.8); await updateAgent( { id: agentId }, @@ -1471,15 +1488,15 @@ describe('models/Agent', () => { ); const complexAgent = await getAgent({ id: agentId }); - expect(complexAgent.versions).toHaveLength(3); - expect(complexAgent.model_parameters.temperature).toBe(0.8); - expect(complexAgent.model_parameters.max_tokens).toBe(1000); + expect(complexAgent!.versions).toHaveLength(3); + expect(complexAgent!.model_parameters?.temperature).toBe(0.8); + expect(complexAgent!.model_parameters?.max_tokens).toBe(1000); await updateAgent({ id: agentId }, { model_parameters: {} }); const emptyParamsAgent = await getAgent({ id: agentId }); - expect(emptyParamsAgent.versions).toHaveLength(4); - expect(emptyParamsAgent.model_parameters).toEqual({}); + expect(emptyParamsAgent!.versions).toHaveLength(4); + expect(emptyParamsAgent!.model_parameters).toEqual({}); }); test('should not create new version for duplicate updates', async () => { @@ -1498,15 +1515,15 @@ describe('models/Agent', () => { }); const updatedAgent = await updateAgent({ id: testAgentId }, testCase.update); - expect(updatedAgent.versions).toHaveLength(2); // No new version created + expect(updatedAgent!.versions).toHaveLength(2); // No new version created // Update with duplicate data should succeed but not create a new version const duplicateUpdate = await updateAgent({ id: testAgentId }, testCase.duplicate); - expect(duplicateUpdate.versions).toHaveLength(2); // No new version created + expect(duplicateUpdate!.versions).toHaveLength(2); // No new version created const agent = await getAgent({ id: testAgentId }); - expect(agent.versions).toHaveLength(2); + expect(agent!.versions).toHaveLength(2); } }); @@ -1530,9 +1547,11 @@ describe('models/Agent', () => { { updatingUserId: updatingUser.toString() }, ); - expect(updatedAgent.versions).toHaveLength(2); - expect(updatedAgent.versions[1].updatedBy.toString()).toBe(updatingUser.toString()); - expect(updatedAgent.author.toString()).toBe(originalAuthor.toString()); + expect(updatedAgent!.versions).toHaveLength(2); + expect((updatedAgent!.versions![1] as VersionEntry)?.updatedBy?.toString()).toBe( + updatingUser.toString(), + ); + expect(updatedAgent!.author.toString()).toBe(originalAuthor.toString()); }); test('should include updatedBy even when the original author updates the agent', async () => { @@ -1554,9 +1573,11 @@ describe('models/Agent', () => { { updatingUserId: originalAuthor.toString() }, ); - expect(updatedAgent.versions).toHaveLength(2); - expect(updatedAgent.versions[1].updatedBy.toString()).toBe(originalAuthor.toString()); - expect(updatedAgent.author.toString()).toBe(originalAuthor.toString()); + expect(updatedAgent!.versions).toHaveLength(2); + expect((updatedAgent!.versions![1] as VersionEntry)?.updatedBy?.toString()).toBe( + originalAuthor.toString(), + ); + expect(updatedAgent!.author.toString()).toBe(originalAuthor.toString()); }); test('should track multiple different users updating the same agent', async () => { @@ -1603,20 +1624,21 @@ describe('models/Agent', () => { { updatingUserId: user3.toString() }, ); - expect(finalAgent.versions).toHaveLength(5); - expect(finalAgent.author.toString()).toBe(originalAuthor.toString()); + expect(finalAgent!.versions).toHaveLength(5); + expect(finalAgent!.author.toString()).toBe(originalAuthor.toString()); // Check that each version has the correct updatedBy - expect(finalAgent.versions[0].updatedBy).toBeUndefined(); // Initial creation has no updatedBy - expect(finalAgent.versions[1].updatedBy.toString()).toBe(user1.toString()); - expect(finalAgent.versions[2].updatedBy.toString()).toBe(originalAuthor.toString()); - expect(finalAgent.versions[3].updatedBy.toString()).toBe(user2.toString()); - expect(finalAgent.versions[4].updatedBy.toString()).toBe(user3.toString()); + const versions = finalAgent!.versions! as VersionEntry[]; + expect(versions[0]?.updatedBy).toBeUndefined(); // Initial creation has no updatedBy + expect(versions[1]?.updatedBy?.toString()).toBe(user1.toString()); + expect(versions[2]?.updatedBy?.toString()).toBe(originalAuthor.toString()); + expect(versions[3]?.updatedBy?.toString()).toBe(user2.toString()); + expect(versions[4]?.updatedBy?.toString()).toBe(user3.toString()); // Verify the final state - expect(finalAgent.name).toBe('Updated by User 2'); - expect(finalAgent.description).toBe('Final update by User 3'); - expect(finalAgent.model).toBe('new-model'); + expect(finalAgent!.name).toBe('Updated by User 2'); + expect(finalAgent!.description).toBe('Final update by User 3'); + expect(finalAgent!.model).toBe('new-model'); }); test('should preserve original author during agent restoration', async () => { @@ -1639,7 +1661,6 @@ describe('models/Agent', () => { { updatingUserId: updatingUser.toString() }, ); - const { revertAgentVersion } = require('./Agent'); const revertedAgent = await revertAgentVersion({ id: agentId }, 0); expect(revertedAgent.author.toString()).toBe(originalAuthor.toString()); @@ -1670,7 +1691,7 @@ describe('models/Agent', () => { { updatingUserId: authorId.toString(), forceVersion: true }, ); - expect(firstUpdate.versions).toHaveLength(2); + expect(firstUpdate!.versions).toHaveLength(2); // Second update with same data but forceVersion should still create a version const secondUpdate = await updateAgent( @@ -1679,7 +1700,7 @@ describe('models/Agent', () => { { updatingUserId: authorId.toString(), forceVersion: true }, ); - expect(secondUpdate.versions).toHaveLength(3); + expect(secondUpdate!.versions).toHaveLength(3); // Update without forceVersion and no changes should not create a version const duplicateUpdate = await updateAgent( @@ -1688,7 +1709,7 @@ describe('models/Agent', () => { { updatingUserId: authorId.toString(), forceVersion: false }, ); - expect(duplicateUpdate.versions).toHaveLength(3); // No new version created + expect(duplicateUpdate!.versions).toHaveLength(3); // No new version created }); test('should handle isDuplicateVersion with arrays containing null/undefined values', async () => { @@ -1707,8 +1728,8 @@ describe('models/Agent', () => { // Update with same array but different null/undefined arrangement const updatedAgent = await updateAgent({ id: agentId }, { tools: ['tool1', 'tool2'] }); - expect(updatedAgent.versions).toHaveLength(2); - expect(updatedAgent.tools).toEqual(['tool1', 'tool2']); + expect(updatedAgent!.versions).toHaveLength(2); + expect(updatedAgent!.tools).toEqual(['tool1', 'tool2']); }); test('should handle isDuplicateVersion with empty objects in tool_kwargs', async () => { @@ -1741,7 +1762,7 @@ describe('models/Agent', () => { ); // Should create new version as order matters for arrays - expect(updatedAgent.versions).toHaveLength(2); + expect(updatedAgent!.versions).toHaveLength(2); }); test('should handle isDuplicateVersion with mixed primitive and object arrays', async () => { @@ -1764,7 +1785,7 @@ describe('models/Agent', () => { ); // Should create new version as types differ - expect(updatedAgent.versions).toHaveLength(2); + expect(updatedAgent!.versions).toHaveLength(2); }); test('should handle isDuplicateVersion with deeply nested objects', async () => { @@ -1808,7 +1829,7 @@ describe('models/Agent', () => { // Since we're updating back to the same model_parameters but with a different description, // it should create a new version const agent = await getAgent({ id: agentId }); - expect(agent.versions).toHaveLength(3); + expect(agent!.versions).toHaveLength(3); }); test('should handle version comparison with special field types', async () => { @@ -1827,7 +1848,7 @@ describe('models/Agent', () => { // Update with a real field change first const firstUpdate = await updateAgent({ id: agentId }, { description: 'New description' }); - expect(firstUpdate.versions).toHaveLength(2); + expect(firstUpdate!.versions).toHaveLength(2); // Update with model parameters change const secondUpdate = await updateAgent( @@ -1835,7 +1856,7 @@ describe('models/Agent', () => { { model_parameters: { temperature: 0.8 } }, ); - expect(secondUpdate.versions).toHaveLength(3); + expect(secondUpdate!.versions).toHaveLength(3); }); test('should detect changes in support_contact fields', async () => { @@ -1866,9 +1887,9 @@ describe('models/Agent', () => { }, ); - expect(firstUpdate.versions).toHaveLength(2); - expect(firstUpdate.support_contact.name).toBe('Updated Support'); - expect(firstUpdate.support_contact.email).toBe('initial@support.com'); + expect(firstUpdate!.versions).toHaveLength(2); + expect(firstUpdate!.support_contact?.name).toBe('Updated Support'); + expect(firstUpdate!.support_contact?.email).toBe('initial@support.com'); // Update support_contact email only const secondUpdate = await updateAgent( @@ -1881,8 +1902,8 @@ describe('models/Agent', () => { }, ); - expect(secondUpdate.versions).toHaveLength(3); - expect(secondUpdate.support_contact.email).toBe('updated@support.com'); + expect(secondUpdate!.versions).toHaveLength(3); + expect(secondUpdate!.support_contact?.email).toBe('updated@support.com'); // Try to update with same support_contact - should be detected as duplicate but return successfully const duplicateUpdate = await updateAgent( @@ -1896,9 +1917,9 @@ describe('models/Agent', () => { ); // Should not create a new version - expect(duplicateUpdate.versions).toHaveLength(3); - expect(duplicateUpdate.version).toBe(3); - expect(duplicateUpdate.support_contact.email).toBe('updated@support.com'); + expect(duplicateUpdate?.versions).toHaveLength(3); + expect((duplicateUpdate as IAgent & { version?: number })?.version).toBe(3); + expect(duplicateUpdate?.support_contact?.email).toBe('updated@support.com'); }); test('should handle support_contact from empty to populated', async () => { @@ -1928,9 +1949,9 @@ describe('models/Agent', () => { }, ); - expect(updated.versions).toHaveLength(2); - expect(updated.support_contact.name).toBe('New Support Team'); - expect(updated.support_contact.email).toBe('support@example.com'); + expect(updated?.versions).toHaveLength(2); + expect(updated?.support_contact?.name).toBe('New Support Team'); + expect(updated?.support_contact?.email).toBe('support@example.com'); }); test('should handle support_contact edge cases in isDuplicateVersion', async () => { @@ -1958,8 +1979,8 @@ describe('models/Agent', () => { }, ); - expect(emptyUpdate.versions).toHaveLength(2); - expect(emptyUpdate.support_contact).toEqual({}); + expect(emptyUpdate?.versions).toHaveLength(2); + expect(emptyUpdate?.support_contact).toEqual({}); // Update back to populated support_contact const repopulated = await updateAgent( @@ -1972,16 +1993,16 @@ describe('models/Agent', () => { }, ); - expect(repopulated.versions).toHaveLength(3); + expect(repopulated?.versions).toHaveLength(3); // Verify all versions have correct support_contact const finalAgent = await getAgent({ id: agentId }); - expect(finalAgent.versions[0].support_contact).toEqual({ + expect(finalAgent!.versions![0]?.support_contact).toEqual({ name: 'Support', email: 'support@test.com', }); - expect(finalAgent.versions[1].support_contact).toEqual({}); - expect(finalAgent.versions[2].support_contact).toEqual({ + expect(finalAgent!.versions![1]?.support_contact).toEqual({}); + expect(finalAgent!.versions![2]?.support_contact).toEqual({ name: 'Support', email: 'support@test.com', }); @@ -2028,22 +2049,22 @@ describe('models/Agent', () => { const finalAgent = await getAgent({ id: agentId }); // Verify version history - expect(finalAgent.versions).toHaveLength(3); - expect(finalAgent.versions[0].support_contact).toEqual({ + expect(finalAgent!.versions).toHaveLength(3); + expect(finalAgent!.versions![0]?.support_contact).toEqual({ name: 'Initial Contact', email: 'initial@test.com', }); - expect(finalAgent.versions[1].support_contact).toEqual({ + expect(finalAgent!.versions![1]?.support_contact).toEqual({ name: 'Second Contact', email: 'second@test.com', }); - expect(finalAgent.versions[2].support_contact).toEqual({ + expect(finalAgent!.versions![2]?.support_contact).toEqual({ name: 'Third Contact', email: 'third@test.com', }); // Current state should match last version - expect(finalAgent.support_contact).toEqual({ + expect(finalAgent!.support_contact).toEqual({ name: 'Third Contact', email: 'third@test.com', }); @@ -2078,9 +2099,9 @@ describe('models/Agent', () => { }, ); - expect(updated.versions).toHaveLength(2); - expect(updated.support_contact.name).toBe('New Name'); - expect(updated.support_contact.email).toBe(''); + expect(updated?.versions).toHaveLength(2); + expect(updated?.support_contact?.name).toBe('New Name'); + expect(updated?.support_contact?.email).toBe(''); // Verify isDuplicateVersion works with partial changes - should return successfully without creating new version const duplicateUpdate = await updateAgent( @@ -2094,10 +2115,10 @@ describe('models/Agent', () => { ); // Should not create a new version since content is the same - expect(duplicateUpdate.versions).toHaveLength(2); - expect(duplicateUpdate.version).toBe(2); - expect(duplicateUpdate.support_contact.name).toBe('New Name'); - expect(duplicateUpdate.support_contact.email).toBe(''); + expect(duplicateUpdate?.versions).toHaveLength(2); + expect((duplicateUpdate as IAgent & { version?: number })?.version).toBe(2); + expect(duplicateUpdate?.support_contact?.name).toBe('New Name'); + expect(duplicateUpdate?.support_contact?.email).toBe(''); }); // Edge Cases @@ -2120,7 +2141,7 @@ describe('models/Agent', () => { ])('addAgentResourceFile with $name', ({ needsAgent, params, shouldResolve, error }) => { test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { const agent = needsAgent ? await createBasicAgent() : null; - const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; + const agent_id = needsAgent ? agent!.id : `agent_${uuidv4()}`; if (shouldResolve) { await expect(addAgentResourceFile({ agent_id, ...params })).resolves.toBeDefined(); @@ -2153,7 +2174,7 @@ describe('models/Agent', () => { ])('removeAgentResourceFiles with $name', ({ files, needsAgent, shouldResolve, error }) => { test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { const agent = needsAgent ? await createBasicAgent() : null; - const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; + const agent_id = needsAgent ? agent!.id : `agent_${uuidv4()}`; if (shouldResolve) { const result = await removeAgentResourceFiles({ agent_id, files }); @@ -2185,8 +2206,8 @@ describe('models/Agent', () => { } const agent = await getAgent({ id: agentId }); - expect(agent.versions).toHaveLength(21); - expect(agent.description).toBe('Version 19'); + expect(agent!.versions).toHaveLength(21); + expect(agent!.description).toBe('Version 19'); }); test('should handle revertAgentVersion with invalid version index', async () => { @@ -2227,27 +2248,13 @@ describe('models/Agent', () => { const updatedAgent = await updateAgent({ id: agentId }, {}); expect(updatedAgent).toBeDefined(); - expect(updatedAgent.name).toBe('Test Agent'); - expect(updatedAgent.versions).toHaveLength(1); + expect(updatedAgent!.name).toBe('Test Agent'); + expect(updatedAgent!.versions).toHaveLength(1); }); }); }); describe('Action Metadata and Hash Generation', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); - beforeEach(async () => { await Agent.deleteMany({}); }); @@ -2415,330 +2422,9 @@ describe('models/Agent', () => { }); }); - describe('Load Agent Functionality', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); - - beforeEach(async () => { - await Agent.deleteMany({}); - }); - - test('should return null when agent_id is not provided', async () => { - const mockReq = { user: { id: 'user123' } }; - const result = await loadAgent({ - req: mockReq, - agent_id: null, - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - expect(result).toBeNull(); - }); - - test('should return null when agent_id is empty string', async () => { - const mockReq = { user: { id: 'user123' } }; - const result = await loadAgent({ - req: mockReq, - agent_id: '', - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - expect(result).toBeNull(); - }); - - test('should test ephemeral agent loading logic', async () => { - const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; - - getCachedTools.mockResolvedValue({ - tool1_mcp_server1: {}, - tool2_mcp_server2: {}, - another_tool: {}, - }); - - // Mock getMCPServerTools to return tools for each server - getMCPServerTools.mockImplementation(async (_userId, server) => { - if (server === 'server1') { - return { tool1_mcp_server1: {} }; - } else if (server === 'server2') { - return { tool2_mcp_server2: {} }; - } - return null; - }); - - const mockReq = { - user: { id: 'user123' }, - body: { - promptPrefix: 'Test instructions', - ephemeralAgent: { - execute_code: true, - web_search: true, - mcp: ['server1', 'server2'], - }, - }, - }; - - const result = await loadAgent({ - req: mockReq, - agent_id: EPHEMERAL_AGENT_ID, - endpoint: 'openai', - model_parameters: { model: 'gpt-4', temperature: 0.7 }, - }); - - if (result) { - // Ephemeral agent ID is encoded with endpoint and model - expect(result.id).toBe('openai__gpt-4'); - expect(result.instructions).toBe('Test instructions'); - expect(result.provider).toBe('openai'); - expect(result.model).toBe('gpt-4'); - expect(result.model_parameters.temperature).toBe(0.7); - expect(result.tools).toContain('execute_code'); - expect(result.tools).toContain('web_search'); - expect(result.tools).toContain('tool1_mcp_server1'); - expect(result.tools).toContain('tool2_mcp_server2'); - } else { - expect(result).toBeNull(); - } - }); - - test('should return null for non-existent agent', async () => { - const mockReq = { user: { id: 'user123' } }; - const result = await loadAgent({ - req: mockReq, - agent_id: 'agent_non_existent', - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - expect(result).toBeNull(); - }); - - test('should load agent when user is the author', async () => { - const userId = new mongoose.Types.ObjectId(); - const agentId = `agent_${uuidv4()}`; - - await createAgent({ - id: agentId, - name: 'Test Agent', - provider: 'openai', - model: 'gpt-4', - author: userId, - description: 'Test description', - tools: ['web_search'], - }); - - const mockReq = { user: { id: userId.toString() } }; - const result = await loadAgent({ - req: mockReq, - agent_id: agentId, - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - expect(result).toBeDefined(); - expect(result.id).toBe(agentId); - expect(result.name).toBe('Test Agent'); - expect(result.author.toString()).toBe(userId.toString()); - expect(result.version).toBe(1); - }); - - test('should return agent even when user is not author (permissions checked at route level)', async () => { - const authorId = new mongoose.Types.ObjectId(); - const userId = new mongoose.Types.ObjectId(); - const agentId = `agent_${uuidv4()}`; - - await createAgent({ - id: agentId, - name: 'Test Agent', - provider: 'openai', - model: 'gpt-4', - author: authorId, - }); - - const mockReq = { user: { id: userId.toString() } }; - const result = await loadAgent({ - req: mockReq, - agent_id: agentId, - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - // With the new permission system, loadAgent returns the agent regardless of permissions - // Permission checks are handled at the route level via middleware - expect(result).toBeTruthy(); - expect(result.id).toBe(agentId); - expect(result.name).toBe('Test Agent'); - }); - - test('should handle ephemeral agent with no MCP servers', async () => { - const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; - - getCachedTools.mockResolvedValue({}); - - const mockReq = { - user: { id: 'user123' }, - body: { - promptPrefix: 'Simple instructions', - ephemeralAgent: { - execute_code: false, - web_search: false, - mcp: [], - }, - }, - }; - - const result = await loadAgent({ - req: mockReq, - agent_id: EPHEMERAL_AGENT_ID, - endpoint: 'openai', - model_parameters: { model: 'gpt-3.5-turbo' }, - }); - - if (result) { - expect(result.tools).toEqual([]); - expect(result.instructions).toBe('Simple instructions'); - } else { - expect(result).toBeFalsy(); - } - }); - - test('should handle ephemeral agent with undefined ephemeralAgent in body', async () => { - const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; - - getCachedTools.mockResolvedValue({}); - - const mockReq = { - user: { id: 'user123' }, - body: { - promptPrefix: 'Basic instructions', - }, - }; - - const result = await loadAgent({ - req: mockReq, - agent_id: EPHEMERAL_AGENT_ID, - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - if (result) { - expect(result.tools).toEqual([]); - } else { - expect(result).toBeFalsy(); - } - }); - - describe('Edge Cases', () => { - test('should handle loadAgent with malformed req object', async () => { - const result = await loadAgent({ - req: null, - agent_id: 'agent_test', - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - expect(result).toBeNull(); - }); - - test('should handle ephemeral agent with extremely large tool list', async () => { - const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; - - const largeToolList = Array.from({ length: 100 }, (_, i) => `tool_${i}_mcp_server1`); - const availableTools = largeToolList.reduce((acc, tool) => { - acc[tool] = {}; - return acc; - }, {}); - - getCachedTools.mockResolvedValue(availableTools); - - // Mock getMCPServerTools to return all tools for server1 - getMCPServerTools.mockImplementation(async (_userId, server) => { - if (server === 'server1') { - return availableTools; // All 100 tools belong to server1 - } - return null; - }); - - const mockReq = { - user: { id: 'user123' }, - body: { - promptPrefix: 'Test', - ephemeralAgent: { - execute_code: true, - web_search: true, - mcp: ['server1'], - }, - }, - }; - - const result = await loadAgent({ - req: mockReq, - agent_id: EPHEMERAL_AGENT_ID, - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - if (result) { - expect(result.tools.length).toBeGreaterThan(100); - } - }); - - test('should return agent from different project (permissions checked at route level)', async () => { - const authorId = new mongoose.Types.ObjectId(); - const userId = new mongoose.Types.ObjectId(); - const agentId = `agent_${uuidv4()}`; - - await createAgent({ - id: agentId, - name: 'Project Agent', - provider: 'openai', - model: 'gpt-4', - author: authorId, - }); - - const mockReq = { user: { id: userId.toString() } }; - const result = await loadAgent({ - req: mockReq, - agent_id: agentId, - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - // With the new permission system, loadAgent returns the agent regardless of permissions - // Permission checks are handled at the route level via middleware - expect(result).toBeTruthy(); - expect(result.id).toBe(agentId); - expect(result.name).toBe('Project Agent'); - }); - }); - }); + /* Load Agent Functionality tests moved to api/models/Agent.spec.js */ describe('Agent Edge Cases and Error Handling', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); - beforeEach(async () => { await Agent.deleteMany({}); }); @@ -2757,8 +2443,8 @@ describe('models/Agent', () => { expect(agent).toBeDefined(); expect(agent.id).toBe(agentId); expect(agent.versions).toHaveLength(1); - expect(agent.versions[0].provider).toBe('test'); - expect(agent.versions[0].model).toBe('test-model'); + expect(agent.versions![0]?.provider).toBe('test'); + expect(agent.versions![0]?.model).toBe('test-model'); }); test('should handle agent creation with all optional fields', async () => { @@ -2788,10 +2474,10 @@ describe('models/Agent', () => { expect(agent.instructions).toBe('Complex instructions'); expect(agent.tools).toEqual(['tool1', 'tool2']); expect(agent.actions).toEqual(['action1', 'action2']); - expect(agent.model_parameters.temperature).toBe(0.8); - expect(agent.model_parameters.max_tokens).toBe(1000); + expect(agent.model_parameters?.temperature).toBe(0.8); + expect(agent.model_parameters?.max_tokens).toBe(1000); expect(agent.avatar).toBe('https://example.com/avatar.png'); - expect(agent.tool_resources.file_search.file_ids).toEqual(['file1', 'file2']); + expect(agent.tool_resources?.file_search?.file_ids).toEqual(['file1', 'file2']); }); test('should handle updateAgent with empty update object', async () => { @@ -2809,8 +2495,8 @@ describe('models/Agent', () => { const updatedAgent = await updateAgent({ id: agentId }, {}); expect(updatedAgent).toBeDefined(); - expect(updatedAgent.name).toBe('Test Agent'); - expect(updatedAgent.versions).toHaveLength(1); // No new version should be created + expect(updatedAgent!.name).toBe('Test Agent'); + expect(updatedAgent!.versions).toHaveLength(1); // No new version should be created }); test('should handle concurrent updates to different agents', async () => { @@ -2840,10 +2526,10 @@ describe('models/Agent', () => { updateAgent({ id: agent2Id }, { description: 'Updated Agent 2' }), ]); - expect(updated1.description).toBe('Updated Agent 1'); - expect(updated2.description).toBe('Updated Agent 2'); - expect(updated1.versions).toHaveLength(2); - expect(updated2.versions).toHaveLength(2); + expect(updated1?.description).toBe('Updated Agent 1'); + expect(updated2?.description).toBe('Updated Agent 2'); + expect(updated1?.versions).toHaveLength(2); + expect(updated2?.versions).toHaveLength(2); }); test('should handle agent deletion with non-existent ID', async () => { @@ -2875,10 +2561,10 @@ describe('models/Agent', () => { }, ); - expect(updatedAgent.name).toBe('Updated Name'); - expect(updatedAgent.tools).toContain('tool1'); - expect(updatedAgent.tools).toContain('tool2'); - expect(updatedAgent.versions).toHaveLength(2); + expect(updatedAgent!.name).toBe('Updated Name'); + expect(updatedAgent!.tools).toContain('tool1'); + expect(updatedAgent!.tools).toContain('tool2'); + expect(updatedAgent!.versions).toHaveLength(2); }); test('should handle revertAgentVersion with invalid version index', async () => { @@ -2905,11 +2591,9 @@ describe('models/Agent', () => { test('should handle addAgentResourceFile with non-existent agent', async () => { const nonExistentId = `agent_${uuidv4()}`; - const mockReq = { user: { id: 'user123' } }; await expect( addAgentResourceFile({ - req: mockReq, agent_id: nonExistentId, tool_resource: 'file_search', file_id: 'file123', @@ -2950,8 +2634,8 @@ describe('models/Agent', () => { }, ); - expect(firstUpdate.tools).toContain('tool1'); - expect(firstUpdate.tools).toContain('tool2'); + expect(firstUpdate!.tools).toContain('tool1'); + expect(firstUpdate!.tools).toContain('tool2'); // Second update with direct field update and $addToSet const secondUpdate = await updateAgent( @@ -2963,13 +2647,13 @@ describe('models/Agent', () => { }, ); - expect(secondUpdate.name).toBe('Updated Agent'); - expect(secondUpdate.model_parameters.temperature).toBe(0.8); - expect(secondUpdate.model_parameters.max_tokens).toBe(500); - expect(secondUpdate.tools).toContain('tool1'); - expect(secondUpdate.tools).toContain('tool2'); - expect(secondUpdate.tools).toContain('tool3'); - expect(secondUpdate.versions).toHaveLength(3); + expect(secondUpdate!.name).toBe('Updated Agent'); + expect(secondUpdate!.model_parameters?.temperature).toBe(0.8); + expect(secondUpdate!.model_parameters?.max_tokens).toBe(500); + expect(secondUpdate!.tools).toContain('tool1'); + expect(secondUpdate!.tools).toContain('tool2'); + expect(secondUpdate!.tools).toContain('tool3'); + expect(secondUpdate!.versions).toHaveLength(3); }); test('should preserve version order in versions array', async () => { @@ -2988,12 +2672,12 @@ describe('models/Agent', () => { await updateAgent({ id: agentId }, { name: 'Version 3' }); const finalAgent = await updateAgent({ id: agentId }, { name: 'Version 4' }); - expect(finalAgent.versions).toHaveLength(4); - expect(finalAgent.versions[0].name).toBe('Version 1'); - expect(finalAgent.versions[1].name).toBe('Version 2'); - expect(finalAgent.versions[2].name).toBe('Version 3'); - expect(finalAgent.versions[3].name).toBe('Version 4'); - expect(finalAgent.name).toBe('Version 4'); + expect(finalAgent!.versions).toHaveLength(4); + expect(finalAgent!.versions![0]?.name).toBe('Version 1'); + expect(finalAgent!.versions![1]?.name).toBe('Version 2'); + expect(finalAgent!.versions![2]?.name).toBe('Version 3'); + expect(finalAgent!.versions![3]?.name).toBe('Version 4'); + expect(finalAgent!.name).toBe('Version 4'); }); test('should handle revertAgentVersion properly', async () => { @@ -3042,8 +2726,8 @@ describe('models/Agent', () => { ); expect(updatedAgent).toBeDefined(); - expect(updatedAgent.description).toBe('Updated description'); - expect(updatedAgent.versions).toHaveLength(2); + expect(updatedAgent!.description).toBe('Updated description'); + expect(updatedAgent!.versions).toHaveLength(2); }); test('should handle updateAgent with combined MongoDB operators', async () => { @@ -3069,10 +2753,10 @@ describe('models/Agent', () => { ); expect(updatedAgent).toBeDefined(); - expect(updatedAgent.name).toBe('Updated Name'); - expect(updatedAgent.tools).toContain('tool1'); - expect(updatedAgent.tools).toContain('tool2'); - expect(updatedAgent.versions).toHaveLength(2); + expect(updatedAgent!.name).toBe('Updated Name'); + expect(updatedAgent!.tools).toContain('tool1'); + expect(updatedAgent!.tools).toContain('tool2'); + expect(updatedAgent!.versions).toHaveLength(2); }); test('should handle updateAgent when agent does not exist', async () => { @@ -3153,54 +2837,6 @@ describe('models/Agent', () => { Agent.findOneAndUpdate = originalFindOneAndUpdate; }); - test('should handle loadEphemeralAgent with malformed MCP tool names', async () => { - const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; - - getCachedTools.mockResolvedValue({ - malformed_tool_name: {}, // No mcp delimiter - tool__server1: {}, // Wrong delimiter - tool_mcp_server1: {}, // Correct format - tool_mcp_server2: {}, // Different server - }); - - // Mock getMCPServerTools to return only tools matching the server - getMCPServerTools.mockImplementation(async (_userId, server) => { - if (server === 'server1') { - // Only return tool that correctly matches server1 format - return { tool_mcp_server1: {} }; - } else if (server === 'server2') { - return { tool_mcp_server2: {} }; - } - return null; - }); - - const mockReq = { - user: { id: 'user123' }, - body: { - promptPrefix: 'Test instructions', - ephemeralAgent: { - execute_code: false, - web_search: false, - mcp: ['server1'], - }, - }, - }; - - const result = await loadAgent({ - req: mockReq, - agent_id: EPHEMERAL_AGENT_ID, - endpoint: 'openai', - model_parameters: { model: 'gpt-4' }, - }); - - if (result) { - expect(result.tools).toEqual(['tool_mcp_server1']); - expect(result.tools).not.toContain('malformed_tool_name'); - expect(result.tools).not.toContain('tool__server1'); - expect(result.tools).not.toContain('tool_mcp_server2'); - } - }); - test('should handle addAgentResourceFile when array initialization fails', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); @@ -3221,7 +2857,10 @@ describe('models/Agent', () => { updateOneCalled = true; return Promise.reject(new Error('Database error')); } - return originalUpdateOne.apply(Agent, args); + return originalUpdateOne.apply( + Agent, + args as [update: UpdateQuery | UpdateWithAggregationPipeline], + ); }); try { @@ -3233,8 +2872,8 @@ describe('models/Agent', () => { expect(result).toBeDefined(); expect(result.tools).toContain('new_tool'); - } catch (error) { - expect(error.message).toBe('Database error'); + } catch (error: unknown) { + expect((error as Error).message).toBe('Database error'); } Agent.updateOne = originalUpdateOne; @@ -3242,20 +2881,6 @@ describe('models/Agent', () => { }); describe('Agent IDs Field in Version Detection', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); - beforeEach(async () => { await Agent.deleteMany({}); }); @@ -3282,8 +2907,8 @@ describe('models/Agent', () => { ); // Since agent_ids is no longer excluded, this should create a new version - expect(updated.versions).toHaveLength(2); - expect(updated.agent_ids).toEqual(['agent1', 'agent2', 'agent3']); + expect(updated?.versions).toHaveLength(2); + expect(updated?.agent_ids).toEqual(['agent1', 'agent2', 'agent3']); }); test('should detect duplicate version if agent_ids is updated to same value', async () => { @@ -3303,14 +2928,14 @@ describe('models/Agent', () => { { id: agentId }, { agent_ids: ['agent1', 'agent2', 'agent3'] }, ); - expect(updatedAgent.versions).toHaveLength(2); + expect(updatedAgent!.versions).toHaveLength(2); // Update with same agent_ids should succeed but not create a new version const duplicateUpdate = await updateAgent( { id: agentId }, { agent_ids: ['agent1', 'agent2', 'agent3'] }, ); - expect(duplicateUpdate.versions).toHaveLength(2); // No new version created + expect(duplicateUpdate?.versions).toHaveLength(2); // No new version created }); test('should handle agent_ids field alongside other fields', async () => { @@ -3335,15 +2960,15 @@ describe('models/Agent', () => { }, ); - expect(updated.versions).toHaveLength(2); - expect(updated.agent_ids).toEqual(['agent1', 'agent2']); - expect(updated.description).toBe('Updated description'); + expect(updated?.versions).toHaveLength(2); + expect(updated?.agent_ids).toEqual(['agent1', 'agent2']); + expect(updated?.description).toBe('Updated description'); const updated2 = await updateAgent({ id: agentId }, { description: 'Another description' }); - expect(updated2.versions).toHaveLength(3); - expect(updated2.agent_ids).toEqual(['agent1', 'agent2']); - expect(updated2.description).toBe('Another description'); + expect(updated2?.versions).toHaveLength(3); + expect(updated2?.agent_ids).toEqual(['agent1', 'agent2']); + expect(updated2?.description).toBe('Another description'); }); test('should preserve agent_ids in version history', async () => { @@ -3365,11 +2990,11 @@ describe('models/Agent', () => { const finalAgent = await getAgent({ id: agentId }); - expect(finalAgent.versions).toHaveLength(3); - expect(finalAgent.versions[0].agent_ids).toEqual(['agent1']); - expect(finalAgent.versions[1].agent_ids).toEqual(['agent1', 'agent2']); - expect(finalAgent.versions[2].agent_ids).toEqual(['agent3']); - expect(finalAgent.agent_ids).toEqual(['agent3']); + expect(finalAgent!.versions).toHaveLength(3); + expect(finalAgent!.versions![0]?.agent_ids).toEqual(['agent1']); + expect(finalAgent!.versions![1]?.agent_ids).toEqual(['agent1', 'agent2']); + expect(finalAgent!.versions![2]?.agent_ids).toEqual(['agent3']); + expect(finalAgent!.agent_ids).toEqual(['agent3']); }); test('should handle empty agent_ids arrays', async () => { @@ -3387,13 +3012,13 @@ describe('models/Agent', () => { const updated = await updateAgent({ id: agentId }, { agent_ids: [] }); - expect(updated.versions).toHaveLength(2); - expect(updated.agent_ids).toEqual([]); + expect(updated?.versions).toHaveLength(2); + expect(updated?.agent_ids).toEqual([]); // Update with same empty agent_ids should succeed but not create a new version const duplicateUpdate = await updateAgent({ id: agentId }, { agent_ids: [] }); - expect(duplicateUpdate.versions).toHaveLength(2); // No new version created - expect(duplicateUpdate.agent_ids).toEqual([]); + expect(duplicateUpdate?.versions).toHaveLength(2); // No new version created + expect(duplicateUpdate?.agent_ids).toEqual([]); }); test('should handle agent without agent_ids field', async () => { @@ -3412,27 +3037,13 @@ describe('models/Agent', () => { const updated = await updateAgent({ id: agentId }, { agent_ids: ['agent1'] }); - expect(updated.versions).toHaveLength(2); - expect(updated.agent_ids).toEqual(['agent1']); + expect(updated?.versions).toHaveLength(2); + expect(updated?.agent_ids).toEqual(['agent1']); }); }); }); describe('Support Contact Field', () => { - let mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); - await mongoose.connect(mongoUri); - }, 20000); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); - beforeEach(async () => { await Agent.deleteMany({}); }); @@ -3456,18 +3067,18 @@ describe('Support Contact Field', () => { // Verify support_contact is stored correctly expect(agent.support_contact).toBeDefined(); - expect(agent.support_contact.name).toBe('Support Team'); - expect(agent.support_contact.email).toBe('support@example.com'); + expect(agent.support_contact?.name).toBe('Support Team'); + expect(agent.support_contact?.email).toBe('support@example.com'); // Verify no _id field is created in support_contact - expect(agent.support_contact._id).toBeUndefined(); + expect((agent.support_contact as Record)?._id).toBeUndefined(); // Fetch from database to double-check const dbAgent = await Agent.findOne({ id: agentData.id }); - expect(dbAgent.support_contact).toBeDefined(); - expect(dbAgent.support_contact.name).toBe('Support Team'); - expect(dbAgent.support_contact.email).toBe('support@example.com'); - expect(dbAgent.support_contact._id).toBeUndefined(); + expect(dbAgent?.support_contact).toBeDefined(); + expect(dbAgent?.support_contact?.name).toBe('Support Team'); + expect(dbAgent?.support_contact?.email).toBe('support@example.com'); + expect((dbAgent?.support_contact as Record)?._id).toBeUndefined(); }); it('should handle empty support_contact correctly', async () => { @@ -3485,7 +3096,7 @@ describe('Support Contact Field', () => { // Verify empty support_contact is stored as empty object expect(agent.support_contact).toEqual({}); - expect(agent.support_contact._id).toBeUndefined(); + expect((agent.support_contact as Record)?._id).toBeUndefined(); }); it('should handle missing support_contact correctly', async () => { @@ -3505,11 +3116,12 @@ describe('Support Contact Field', () => { }); describe('getListAgentsByAccess - Security Tests', () => { - let userA, userB; - let agentA1, agentA2, agentA3; + let userA: mongoose.Types.ObjectId, userB: mongoose.Types.ObjectId; + let agentA1: Awaited>, + agentA2: Awaited>, + agentA3: Awaited>; beforeEach(async () => { - Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); await Agent.deleteMany({}); await AclEntry.deleteMany({}); @@ -3572,7 +3184,7 @@ describe('Support Contact Field', () => { test('should only return agents in accessibleIds list', async () => { // Give User B access to only one of User A's agents - const accessibleIds = [agentA1._id]; + const accessibleIds = [agentA1._id] as mongoose.Types.ObjectId[]; const result = await getListAgentsByAccess({ accessibleIds, @@ -3586,7 +3198,7 @@ describe('Support Contact Field', () => { test('should return multiple accessible agents when provided', async () => { // Give User B access to two of User A's agents - const accessibleIds = [agentA1._id, agentA3._id]; + const accessibleIds = [agentA1._id, agentA3._id] as mongoose.Types.ObjectId[]; const result = await getListAgentsByAccess({ accessibleIds, @@ -3602,7 +3214,7 @@ describe('Support Contact Field', () => { test('should respect other query parameters while enforcing accessibleIds', async () => { // Give access to all agents but filter by name - const accessibleIds = [agentA1._id, agentA2._id, agentA3._id]; + const accessibleIds = [agentA1._id, agentA2._id, agentA3._id] as mongoose.Types.ObjectId[]; const result = await getListAgentsByAccess({ accessibleIds, @@ -3629,7 +3241,9 @@ describe('Support Contact Field', () => { } // Give access to all agents - const allAgentIds = [agentA1, agentA2, agentA3, ...moreAgents].map((a) => a._id); + const allAgentIds = [agentA1, agentA2, agentA3, ...moreAgents].map( + (a) => a._id, + ) as mongoose.Types.ObjectId[]; // First page const page1 = await getListAgentsByAccess({ @@ -3696,7 +3310,7 @@ describe('Support Contact Field', () => { }); // Give User B access to one of User A's agents - const accessibleIds = [agentA1._id, agentB1._id]; + const accessibleIds = [agentA1._id, agentB1._id] as mongoose.Types.ObjectId[]; // Filter by author should further restrict the results const result = await getListAgentsByAccess({ @@ -3730,13 +3344,17 @@ function createTestIds() { }; } -function createFileOperations(agentId, fileIds, operation = 'add') { +function createFileOperations(agentId: string, fileIds: string[], operation = 'add') { return fileIds.map((fileId) => operation === 'add' - ? addAgentResourceFile({ agent_id: agentId, tool_resource: 'test_tool', file_id: fileId }) + ? addAgentResourceFile({ + agent_id: agentId, + tool_resource: EToolResources.execute_code, + file_id: fileId, + }) : removeAgentResourceFiles({ agent_id: agentId, - files: [{ tool_resource: 'test_tool', file_id: fileId }], + files: [{ tool_resource: EToolResources.execute_code, file_id: fileId }], }), ); } @@ -3750,7 +3368,14 @@ function mockFindOneAndUpdateError(errorOnCall = 1) { if (callCount === errorOnCall) { throw new Error('Database connection lost'); } - return original.apply(Agent, args); + return original.apply( + Agent, + args as [ + filter?: RootFilterQuery | undefined, + update?: UpdateQuery | undefined, + options?: QueryOptions | null | undefined, + ], + ); }); return () => { diff --git a/packages/data-schemas/src/methods/agent.ts b/packages/data-schemas/src/methods/agent.ts new file mode 100644 index 0000000000..43147eaea9 --- /dev/null +++ b/packages/data-schemas/src/methods/agent.ts @@ -0,0 +1,762 @@ +import crypto from 'node:crypto'; +import type { FilterQuery, Model, Types } from 'mongoose'; +import { Constants, ResourceType, actionDelimiter } from 'librechat-data-provider'; +import logger from '~/config/winston'; +import type { IAgent } from '~/types'; + +const { mcp_delimiter } = Constants; + +export interface AgentDeps { + /** Removes all ACL permissions for a resource. Injected from PermissionService. */ + removeAllPermissions: (params: { resourceType: string; resourceId: unknown }) => Promise; + /** Gets actions. Created by createActionMethods. */ + getActions: ( + searchParams: FilterQuery, + includeSensitive?: boolean, + ) => Promise; + /** Returns resource IDs solely owned by the given user. From createAclEntryMethods. */ + getSoleOwnedResourceIds: ( + userObjectId: Types.ObjectId, + resourceTypes: string | string[], + ) => Promise; +} + +/** + * Extracts unique MCP server names from tools array. + * Tools format: "toolName_mcp_serverName" or "sys__server__sys_mcp_serverName" + */ +function extractMCPServerNames(tools: string[] | undefined | null): string[] { + if (!tools || !Array.isArray(tools)) { + return []; + } + const serverNames = new Set(); + for (const tool of tools) { + if (!tool || !tool.includes(mcp_delimiter)) { + continue; + } + const parts = tool.split(mcp_delimiter); + if (parts.length >= 2) { + serverNames.add(parts[parts.length - 1]); + } + } + return Array.from(serverNames); +} + +/** + * Check if a version already exists in the versions array, excluding timestamp and author fields. + */ +function isDuplicateVersion( + updateData: Record, + currentData: Record, + versions: Record[], + actionsHash: string | null = null, +): Record | null { + if (!versions || versions.length === 0) { + return null; + } + + const excludeFields = [ + '_id', + 'id', + 'createdAt', + 'updatedAt', + 'author', + 'updatedBy', + 'created_at', + 'updated_at', + '__v', + 'versions', + 'actionsHash', + ]; + + const { $push: _$push, $pull: _$pull, $addToSet: _$addToSet, ...directUpdates } = updateData; + + if (Object.keys(directUpdates).length === 0 && !actionsHash) { + return null; + } + + const wouldBeVersion = { ...currentData, ...directUpdates } as Record; + const lastVersion = versions[versions.length - 1] as Record; + + if (actionsHash && lastVersion.actionsHash !== actionsHash) { + return null; + } + + const allFields = new Set([...Object.keys(wouldBeVersion), ...Object.keys(lastVersion)]); + const importantFields = Array.from(allFields).filter((field) => !excludeFields.includes(field)); + + let isMatch = true; + for (const field of importantFields) { + const wouldBeValue = wouldBeVersion[field]; + const lastVersionValue = lastVersion[field]; + + if (!wouldBeValue && !lastVersionValue) { + continue; + } + + // Handle arrays + if (Array.isArray(wouldBeValue) || Array.isArray(lastVersionValue)) { + let wouldBeArr: unknown[]; + if (Array.isArray(wouldBeValue)) { + wouldBeArr = wouldBeValue; + } else if (wouldBeValue == null) { + wouldBeArr = []; + } else { + wouldBeArr = [wouldBeValue]; + } + + let lastVersionArr: unknown[]; + if (Array.isArray(lastVersionValue)) { + lastVersionArr = lastVersionValue; + } else if (lastVersionValue == null) { + lastVersionArr = []; + } else { + lastVersionArr = [lastVersionValue]; + } + + if (wouldBeArr.length !== lastVersionArr.length) { + isMatch = false; + break; + } + + if (wouldBeArr.length > 0 && typeof wouldBeArr[0] === 'object' && wouldBeArr[0] !== null) { + const sortedWouldBe = [...wouldBeArr].map((item) => JSON.stringify(item)).sort(); + const sortedVersion = [...lastVersionArr].map((item) => JSON.stringify(item)).sort(); + + if (!sortedWouldBe.every((item, i) => item === sortedVersion[i])) { + isMatch = false; + break; + } + } else { + const sortedWouldBe = [...wouldBeArr].sort() as string[]; + const sortedVersion = [...lastVersionArr].sort() as string[]; + + if (!sortedWouldBe.every((item, i) => item === sortedVersion[i])) { + isMatch = false; + break; + } + } + } + // Handle objects + else if (typeof wouldBeValue === 'object' && wouldBeValue !== null) { + const lastVersionObj = + typeof lastVersionValue === 'object' && lastVersionValue !== null ? lastVersionValue : {}; + + const wouldBeKeys = Object.keys(wouldBeValue as Record); + const lastVersionKeys = Object.keys(lastVersionObj as Record); + + if (wouldBeKeys.length === 0 && lastVersionKeys.length === 0) { + continue; + } + + if (JSON.stringify(wouldBeValue) !== JSON.stringify(lastVersionObj)) { + isMatch = false; + break; + } + } + // Handle primitive values + else { + if (wouldBeValue !== lastVersionValue) { + if ( + typeof wouldBeValue === 'boolean' && + wouldBeValue === false && + lastVersionValue === undefined + ) { + continue; + } + if ( + typeof wouldBeValue === 'string' && + wouldBeValue === '' && + lastVersionValue === undefined + ) { + continue; + } + isMatch = false; + break; + } + } + } + + return isMatch ? lastVersion : null; +} + +/** + * Generates a hash of action metadata for version comparison. + */ +async function generateActionMetadataHash( + actionIds: string[] | null | undefined, + actions: Array<{ action_id: string; metadata: Record | null }>, +): Promise { + if (!actionIds || actionIds.length === 0) { + return ''; + } + + const actionMap = new Map | null>(); + actions.forEach((action) => { + actionMap.set(action.action_id, action.metadata); + }); + + const sortedActionIds = [...actionIds].sort(); + + const metadataString = sortedActionIds + .map((actionFullId) => { + const parts = actionFullId.split(actionDelimiter); + const actionId = parts[1]; + + const metadata = actionMap.get(actionId); + if (!metadata) { + return `${actionId}:null`; + } + + const sortedKeys = Object.keys(metadata).sort(); + const metadataStr = sortedKeys + .map((key) => `${key}:${JSON.stringify(metadata[key])}`) + .join(','); + return `${actionId}:{${metadataStr}}`; + }) + .join(';'); + + const encoder = new TextEncoder(); + const data = encoder.encode(metadataString); + const hashBuffer = await crypto.webcrypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + + return hashHex; +} + +export function createAgentMethods(mongoose: typeof import('mongoose'), deps: AgentDeps) { + const { removeAllPermissions, getActions, getSoleOwnedResourceIds } = deps; + + /** + * Create an agent with the provided data. + */ + async function createAgent(agentData: Record): Promise { + const Agent = mongoose.models.Agent as Model; + const { author: _author, ...versionData } = agentData; + const timestamp = new Date(); + const initialAgentData = { + ...agentData, + versions: [ + { + ...versionData, + createdAt: timestamp, + updatedAt: timestamp, + }, + ], + category: (agentData.category as string) || 'general', + mcpServerNames: extractMCPServerNames(agentData.tools as string[] | undefined), + }; + + return (await Agent.create(initialAgentData)).toObject() as IAgent; + } + + /** + * Get an agent document based on the provided search parameter. + */ + async function getAgent(searchParameter: FilterQuery): Promise { + const Agent = mongoose.models.Agent as Model; + return (await Agent.findOne(searchParameter).lean()) as IAgent | null; + } + + /** + * Get multiple agent documents based on the provided search parameters. + */ + async function getAgents(searchParameter: FilterQuery): Promise { + const Agent = mongoose.models.Agent as Model; + return (await Agent.find(searchParameter).lean()) as IAgent[]; + } + + /** + * Update an agent with new data without overwriting existing properties, + * or create a new agent if it doesn't exist. + * When an agent is updated, a copy of the current state will be saved to the versions array. + */ + async function updateAgent( + searchParameter: FilterQuery, + updateData: Record, + options: { + updatingUserId?: string | null; + forceVersion?: boolean; + skipVersioning?: boolean; + } = {}, + ): Promise { + const Agent = mongoose.models.Agent as Model; + const { updatingUserId = null, forceVersion = false, skipVersioning = false } = options; + const mongoOptions = { new: true, upsert: false }; + + const currentAgent = await Agent.findOne(searchParameter); + if (currentAgent) { + const { + __v, + _id, + id: __id, + versions, + author: _author, + ...versionData + } = currentAgent.toObject() as unknown as Record; + const { $push, $pull, $addToSet, ...directUpdates } = updateData; + + // Sync mcpServerNames when tools are updated + if ((directUpdates as Record).tools !== undefined) { + const mcpServerNames = extractMCPServerNames( + (directUpdates as Record).tools as string[], + ); + (directUpdates as Record).mcpServerNames = mcpServerNames; + updateData.mcpServerNames = mcpServerNames; + } + + let actionsHash: string | null = null; + + // Generate actions hash if agent has actions + if (currentAgent.actions && currentAgent.actions.length > 0) { + const actionIds = currentAgent.actions + .map((action: string) => { + const parts = action.split(actionDelimiter); + return parts[1]; + }) + .filter(Boolean); + + if (actionIds.length > 0) { + try { + const actions = await getActions({ action_id: { $in: actionIds } }, true); + + actionsHash = await generateActionMetadataHash( + currentAgent.actions, + actions as Array<{ action_id: string; metadata: Record | null }>, + ); + } catch (error) { + logger.error('Error fetching actions for hash generation:', error); + } + } + } + + const shouldCreateVersion = + !skipVersioning && + (forceVersion || Object.keys(directUpdates).length > 0 || $push || $pull || $addToSet); + + if (shouldCreateVersion) { + const duplicateVersion = isDuplicateVersion( + updateData, + versionData, + versions as Record[], + actionsHash, + ); + if (duplicateVersion && !forceVersion) { + const agentObj = currentAgent.toObject() as IAgent & { + version?: number; + versions?: unknown[]; + }; + agentObj.version = (versions as unknown[]).length; + return agentObj; + } + } + + const versionEntry: Record = { + ...versionData, + ...directUpdates, + updatedAt: new Date(), + }; + + if (actionsHash) { + versionEntry.actionsHash = actionsHash; + } + + if (updatingUserId) { + versionEntry.updatedBy = new mongoose.Types.ObjectId(updatingUserId); + } + + if (shouldCreateVersion) { + updateData.$push = { + ...(($push as Record) || {}), + versions: versionEntry, + }; + } + } + + return (await Agent.findOneAndUpdate( + searchParameter, + updateData, + mongoOptions, + ).lean()) as IAgent | null; + } + + /** + * Modifies an agent with the resource file id. + */ + async function addAgentResourceFile({ + agent_id, + tool_resource, + file_id, + updatingUserId, + }: { + agent_id: string; + tool_resource: string; + file_id: string; + updatingUserId?: string; + }): Promise { + const Agent = mongoose.models.Agent as Model; + const searchParameter = { id: agent_id }; + const agent = await getAgent(searchParameter); + if (!agent) { + throw new Error('Agent not found for adding resource file'); + } + const fileIdsPath = `tool_resources.${tool_resource}.file_ids`; + await Agent.updateOne( + { + id: agent_id, + [`${fileIdsPath}`]: { $exists: false }, + }, + { + $set: { + [`${fileIdsPath}`]: [], + }, + }, + ); + + const updateDataObj: Record = { + $addToSet: { + tools: tool_resource, + [fileIdsPath]: file_id, + }, + }; + + const updatedAgent = await updateAgent(searchParameter, updateDataObj, { + updatingUserId, + }); + if (updatedAgent) { + return updatedAgent; + } else { + throw new Error('Agent not found for adding resource file'); + } + } + + /** + * Removes multiple resource files from an agent using atomic operations. + */ + async function removeAgentResourceFiles({ + agent_id, + files, + }: { + agent_id: string; + files: Array<{ tool_resource: string; file_id: string }>; + }): Promise { + const Agent = mongoose.models.Agent as Model; + const searchParameter = { id: agent_id }; + + const filesByResource = files.reduce( + (acc: Record, { tool_resource, file_id }) => { + if (!acc[tool_resource]) { + acc[tool_resource] = []; + } + acc[tool_resource].push(file_id); + return acc; + }, + {}, + ); + + const pullAllOps: Record = {}; + for (const [resource, fileIds] of Object.entries(filesByResource)) { + const fileIdsPath = `tool_resources.${resource}.file_ids`; + pullAllOps[fileIdsPath] = fileIds; + } + + const updatePullData = { $pullAll: pullAllOps }; + const agentAfterPull = (await Agent.findOneAndUpdate(searchParameter, updatePullData, { + new: true, + }).lean()) as IAgent | null; + + if (!agentAfterPull) { + const agentExists = await getAgent(searchParameter); + if (!agentExists) { + throw new Error('Agent not found for removing resource files'); + } + throw new Error('Failed to update agent during file removal (pull step)'); + } + + return agentAfterPull; + } + + /** + * Deletes an agent based on the provided search parameter. + */ + async function deleteAgent(searchParameter: FilterQuery): Promise { + const Agent = mongoose.models.Agent as Model; + const User = mongoose.models.User as Model; + const agent = await Agent.findOneAndDelete(searchParameter); + if (agent) { + await Promise.all([ + removeAllPermissions({ + resourceType: ResourceType.AGENT, + resourceId: agent._id, + }), + removeAllPermissions({ + resourceType: ResourceType.REMOTE_AGENT, + resourceId: agent._id, + }), + ]); + try { + await Agent.updateMany( + { 'edges.to': (agent as unknown as { id: string }).id }, + { $pull: { edges: { to: (agent as unknown as { id: string }).id } } }, + ); + } catch (error) { + logger.error('[deleteAgent] Error removing agent from handoff edges', error); + } + try { + await User.updateMany( + { 'favorites.agentId': (agent as unknown as { id: string }).id }, + { $pull: { favorites: { agentId: (agent as unknown as { id: string }).id } } }, + ); + } catch (error) { + logger.error('[deleteAgent] Error removing agent from user favorites', error); + } + } + return agent ? (agent.toObject() as IAgent) : null; + } + + /** + * Deletes agents solely owned by the user and cleans up their ACLs. + * Agents with other owners are left intact; the caller is responsible for + * removing the user's own ACL principal entries separately. + * + * Also handles legacy (pre-ACL) agents that only have the author field set, + * ensuring they are not orphaned if no permission migration has been run. + */ + async function deleteUserAgents(userId: string): Promise { + const Agent = mongoose.models.Agent as Model; + const AclEntry = mongoose.models.AclEntry as Model; + const User = mongoose.models.User as Model; + + try { + const userObjectId = new mongoose.Types.ObjectId(userId); + const soleOwnedObjectIds = await getSoleOwnedResourceIds(userObjectId, [ + ResourceType.AGENT, + ResourceType.REMOTE_AGENT, + ]); + + const authoredAgents = await Agent.find({ author: userObjectId }).select('id _id').lean(); + + const migratedEntries = + authoredAgents.length > 0 + ? await AclEntry.find({ + resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, + resourceId: { $in: authoredAgents.map((a) => a._id) }, + }) + .select('resourceId') + .lean() + : []; + const migratedIds = new Set( + (migratedEntries as Array<{ resourceId: Types.ObjectId }>).map((e) => e.resourceId.toString()), + ); + const legacyAgents = authoredAgents.filter((a) => !migratedIds.has(a._id.toString())); + + const soleOwnedAgents = + soleOwnedObjectIds.length > 0 + ? await Agent.find({ _id: { $in: soleOwnedObjectIds } }) + .select('id _id') + .lean() + : []; + + const allAgents = [...soleOwnedAgents, ...legacyAgents]; + + if (allAgents.length === 0) { + return; + } + + const agentIds = allAgents.map((agent) => agent.id); + const agentObjectIds = allAgents.map((agent) => agent._id); + + await AclEntry.deleteMany({ + resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, + resourceId: { $in: agentObjectIds }, + }); + + try { + await Agent.updateMany( + { 'edges.to': { $in: agentIds } }, + { $pull: { edges: { to: { $in: agentIds } } } }, + ); + } catch (error) { + logger.error('[deleteUserAgents] Error removing agents from handoff edges', error); + } + + try { + await User.updateMany( + { 'favorites.agentId': { $in: agentIds } }, + { $pull: { favorites: { agentId: { $in: agentIds } } } }, + ); + } catch (error) { + logger.error('[deleteUserAgents] Error removing agents from user favorites', error); + } + + await Agent.deleteMany({ _id: { $in: agentObjectIds } }); + } catch (error) { + logger.error('[deleteUserAgents] General error:', error); + } + } + + /** + * Get agents by accessible IDs with optional cursor-based pagination. + */ + async function getListAgentsByAccess({ + accessibleIds = [], + otherParams = {}, + limit = null, + after = null, + }: { + accessibleIds?: Types.ObjectId[]; + otherParams?: Record; + limit?: number | null; + after?: string | null; + }): Promise<{ + object: string; + data: Array>; + first_id: string | null; + last_id: string | null; + has_more: boolean; + after: string | null; + }> { + const Agent = mongoose.models.Agent as Model; + const isPaginated = limit !== null && limit !== undefined; + const normalizedLimit = isPaginated + ? Math.min(Math.max(1, parseInt(String(limit)) || 20), 100) + : null; + + const baseQuery: Record = { + ...otherParams, + _id: { $in: accessibleIds }, + }; + + if (after) { + try { + const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8')); + const { updatedAt, _id } = cursor; + + const cursorCondition = { + $or: [ + { updatedAt: { $lt: new Date(updatedAt) } }, + { + updatedAt: new Date(updatedAt), + _id: { $gt: new mongoose.Types.ObjectId(_id) }, + }, + ], + }; + + if (Object.keys(baseQuery).length > 0) { + baseQuery.$and = [{ ...baseQuery }, cursorCondition]; + Object.keys(baseQuery).forEach((key) => { + if (key !== '$and') delete baseQuery[key]; + }); + } else { + Object.assign(baseQuery, cursorCondition); + } + } catch (error) { + logger.warn('Invalid cursor:', (error as Error).message); + } + } + + let query = Agent.find(baseQuery, { + id: 1, + _id: 1, + name: 1, + avatar: 1, + author: 1, + description: 1, + updatedAt: 1, + category: 1, + support_contact: 1, + is_promoted: 1, + }).sort({ updatedAt: -1, _id: 1 }); + + if (isPaginated && normalizedLimit) { + query = query.limit(normalizedLimit + 1); + } + + const agents = (await query.lean()) as Array>; + + const hasMore = isPaginated && normalizedLimit ? agents.length > normalizedLimit : false; + const data = (isPaginated && normalizedLimit ? agents.slice(0, normalizedLimit) : agents).map( + (agent) => { + if (agent.author) { + agent.author = (agent.author as Types.ObjectId).toString(); + } + return agent; + }, + ); + + let nextCursor: string | null = null; + if (isPaginated && hasMore && data.length > 0 && normalizedLimit) { + const lastAgent = agents[normalizedLimit - 1]; + nextCursor = Buffer.from( + JSON.stringify({ + updatedAt: (lastAgent.updatedAt as Date).toISOString(), + _id: (lastAgent._id as Types.ObjectId).toString(), + }), + ).toString('base64'); + } + + return { + object: 'list', + data, + first_id: data.length > 0 ? (data[0].id as string) : null, + last_id: data.length > 0 ? (data[data.length - 1].id as string) : null, + has_more: hasMore, + after: nextCursor, + }; + } + + /** + * Reverts an agent to a specific version in its version history. + */ + async function revertAgentVersion( + searchParameter: FilterQuery, + versionIndex: number, + ): Promise { + const Agent = mongoose.models.Agent as Model; + const agent = await Agent.findOne(searchParameter); + if (!agent) { + throw new Error('Agent not found'); + } + + if (!agent.versions || !agent.versions[versionIndex]) { + throw new Error(`Version ${versionIndex} not found`); + } + + const revertToVersion = { ...(agent.versions[versionIndex] as Record) }; + delete revertToVersion._id; + delete revertToVersion.id; + delete revertToVersion.versions; + delete revertToVersion.author; + delete revertToVersion.updatedBy; + + return (await Agent.findOneAndUpdate(searchParameter, revertToVersion, { + new: true, + }).lean()) as IAgent; + } + + /** + * Counts the number of promoted agents. + */ + async function countPromotedAgents(): Promise { + const Agent = mongoose.models.Agent as Model; + return await Agent.countDocuments({ is_promoted: true }); + } + + return { + createAgent, + getAgent, + getAgents, + updateAgent, + deleteAgent, + deleteUserAgents, + revertAgentVersion, + countPromotedAgents, + addAgentResourceFile, + removeAgentResourceFiles, + getListAgentsByAccess, + generateActionMetadataHash, + }; +} + +export type AgentMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/assistant.ts b/packages/data-schemas/src/methods/assistant.ts new file mode 100644 index 0000000000..79133d4237 --- /dev/null +++ b/packages/data-schemas/src/methods/assistant.ts @@ -0,0 +1,69 @@ +import type { FilterQuery, Model } from 'mongoose'; +import type { IAssistant } from '~/types'; + +export function createAssistantMethods(mongoose: typeof import('mongoose')) { + /** + * Update an assistant with new data without overwriting existing properties, + * or create a new assistant if it doesn't exist. + */ + async function updateAssistantDoc( + searchParams: FilterQuery, + updateData: Partial, + ): Promise { + const Assistant = mongoose.models.Assistant as Model; + const options = { new: true, upsert: true }; + return (await Assistant.findOneAndUpdate( + searchParams, + updateData, + options, + ).lean()) as IAssistant | null; + } + + /** + * Retrieves an assistant document based on the provided search params. + */ + async function getAssistant(searchParams: FilterQuery): Promise { + const Assistant = mongoose.models.Assistant as Model; + return (await Assistant.findOne(searchParams).lean()) as IAssistant | null; + } + + /** + * Retrieves all assistants that match the given search parameters. + */ + async function getAssistants( + searchParams: FilterQuery, + select: string | Record | null = null, + ): Promise { + const Assistant = mongoose.models.Assistant as Model; + const query = Assistant.find(searchParams); + + return (await (select ? query.select(select) : query).lean()) as IAssistant[]; + } + + /** + * Deletes an assistant based on the provided search params. + */ + async function deleteAssistant(searchParams: FilterQuery) { + const Assistant = mongoose.models.Assistant as Model; + return await Assistant.findOneAndDelete(searchParams); + } + + /** + * Deletes all assistants matching the given search parameters. + */ + async function deleteAssistants(searchParams: FilterQuery): Promise { + const Assistant = mongoose.models.Assistant as Model; + const result = await Assistant.deleteMany(searchParams); + return result.deletedCount; + } + + return { + updateAssistantDoc, + deleteAssistant, + deleteAssistants, + getAssistants, + getAssistant, + }; +} + +export type AssistantMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/banner.ts b/packages/data-schemas/src/methods/banner.ts new file mode 100644 index 0000000000..6ae4877207 --- /dev/null +++ b/packages/data-schemas/src/methods/banner.ts @@ -0,0 +1,33 @@ +import type { Model } from 'mongoose'; +import logger from '~/config/winston'; +import type { IBanner, IUser } from '~/types'; + +export function createBannerMethods(mongoose: typeof import('mongoose')) { + /** + * Retrieves the current active banner. + */ + async function getBanner(user?: IUser | null): Promise { + try { + const Banner = mongoose.models.Banner as Model; + const now = new Date(); + const banner = (await Banner.findOne({ + displayFrom: { $lte: now }, + $or: [{ displayTo: { $gte: now } }, { displayTo: null }], + type: 'banner', + }).lean()) as IBanner | null; + + if (!banner || banner.isPublic || user != null) { + return banner; + } + + return null; + } catch (error) { + logger.error('[getBanners] Error getting banners', error); + throw new Error('Error getting banners'); + } + } + + return { getBanner }; +} + +export type BannerMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/categories.ts b/packages/data-schemas/src/methods/categories.ts new file mode 100644 index 0000000000..4761a32c16 --- /dev/null +++ b/packages/data-schemas/src/methods/categories.ts @@ -0,0 +1,33 @@ +import logger from '~/config/winston'; + +const options = [ + { label: 'com_ui_idea', value: 'idea' }, + { label: 'com_ui_travel', value: 'travel' }, + { label: 'com_ui_teach_or_explain', value: 'teach_or_explain' }, + { label: 'com_ui_write', value: 'write' }, + { label: 'com_ui_shop', value: 'shop' }, + { label: 'com_ui_code', value: 'code' }, + { label: 'com_ui_misc', value: 'misc' }, + { label: 'com_ui_roleplay', value: 'roleplay' }, + { label: 'com_ui_finance', value: 'finance' }, +] as const; + +export type CategoryOption = { label: string; value: string }; + +export function createCategoriesMethods(_mongoose: typeof import('mongoose')) { + /** + * Retrieves the categories. + */ + async function getCategories(): Promise { + try { + return [...options]; + } catch (error) { + logger.error('Error getting categories', error); + return []; + } + } + + return { getCategories }; +} + +export type CategoriesMethods = ReturnType; diff --git a/api/models/Conversation.spec.js b/packages/data-schemas/src/methods/conversation.spec.ts similarity index 67% rename from api/models/Conversation.spec.js rename to packages/data-schemas/src/methods/conversation.spec.ts index e9e4b5762d..ae19efaf68 100644 --- a/api/models/Conversation.spec.js +++ b/packages/data-schemas/src/methods/conversation.spec.ts @@ -1,39 +1,89 @@ -const mongoose = require('mongoose'); -const { v4: uuidv4 } = require('uuid'); -const { EModelEndpoint } = require('librechat-data-provider'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { - deleteNullOrEmptyConversations, - searchConversation, - getConvosByCursor, - getConvosQueried, - getConvoFiles, - getConvoTitle, - deleteConvos, - saveConvo, - getConvo, -} = require('./Conversation'); -jest.mock('~/server/services/Config/app'); -jest.mock('./Message'); -const { getMessages, deleteMessages } = require('./Message'); +import mongoose from 'mongoose'; +import { v4 as uuidv4 } from 'uuid'; +import { EModelEndpoint } from 'librechat-data-provider'; +import type { IConversation } from '../types'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { ConversationMethods, createConversationMethods } from './conversation'; +import { createModels } from '../models'; -const { Conversation } = require('~/db/models'); +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +})); + +let mongoServer: InstanceType; +let Conversation: mongoose.Model; +let modelsToCleanup: string[] = []; + +// Mock message methods (same as original test mocking ./Message) +const getMessages = jest.fn().mockResolvedValue([]); +const deleteMessages = jest.fn().mockResolvedValue({ deletedCount: 0 }); + +let methods: ConversationMethods; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + + const models = createModels(mongoose); + modelsToCleanup = Object.keys(models); + Object.assign(mongoose.models, models); + Conversation = mongoose.models.Conversation as mongoose.Model; + + methods = createConversationMethods(mongoose, { getMessages, deleteMessages }); + + await mongoose.connect(mongoUri); +}); + +afterAll(async () => { + const collections = mongoose.connection.collections; + for (const key in collections) { + await collections[key].deleteMany({}); + } + + for (const modelName of modelsToCleanup) { + if (mongoose.models[modelName]) { + delete mongoose.models[modelName]; + } + } + + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +const saveConvo = (...args: Parameters) => + methods.saveConvo(...args) as Promise; +const getConvo = (...args: Parameters) => + methods.getConvo(...args); +const getConvoTitle = (...args: Parameters) => + methods.getConvoTitle(...args); +const getConvoFiles = (...args: Parameters) => + methods.getConvoFiles(...args); +const deleteConvos = (...args: Parameters) => + methods.deleteConvos(...args); +const getConvosByCursor = (...args: Parameters) => + methods.getConvosByCursor(...args); +const getConvosQueried = (...args: Parameters) => + methods.getConvosQueried(...args); +const deleteNullOrEmptyConversations = ( + ...args: Parameters +) => methods.deleteNullOrEmptyConversations(...args); +const searchConversation = (...args: Parameters) => + methods.searchConversation(...args); describe('Conversation Operations', () => { - let mongoServer; - let mockReq; - let mockConversationData; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - await mongoose.connect(mongoUri); - }); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); + let mockCtx: { + userId: string; + isTemporary?: boolean; + interfaceConfig?: { temporaryChatRetention?: number }; + }; + let mockConversationData: { + conversationId: string; + title: string; + endpoint: string; + }; beforeEach(async () => { // Clear database @@ -41,18 +91,13 @@ describe('Conversation Operations', () => { // Reset mocks jest.clearAllMocks(); - - // Default mock implementations getMessages.mockResolvedValue([]); deleteMessages.mockResolvedValue({ deletedCount: 0 }); - mockReq = { - user: { id: 'user123' }, - body: {}, - config: { - interfaceConfig: { - temporaryChatRetention: 24, // Default 24 hours - }, + mockCtx = { + userId: 'user123', + interfaceConfig: { + temporaryChatRetention: 24, // Default 24 hours }, }; @@ -65,29 +110,28 @@ describe('Conversation Operations', () => { describe('saveConvo', () => { it('should save a conversation for an authenticated user', async () => { - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); - expect(result.conversationId).toBe(mockConversationData.conversationId); - expect(result.user).toBe('user123'); - expect(result.title).toBe('Test Conversation'); - expect(result.endpoint).toBe(EModelEndpoint.openAI); + expect(result?.conversationId).toBe(mockConversationData.conversationId); + expect(result?.user).toBe('user123'); + expect(result?.title).toBe('Test Conversation'); + expect(result?.endpoint).toBe(EModelEndpoint.openAI); // Verify the conversation was actually saved to the database - const savedConvo = await Conversation.findOne({ + const savedConvo = await Conversation.findOne({ conversationId: mockConversationData.conversationId, user: 'user123', }); expect(savedConvo).toBeTruthy(); - expect(savedConvo.title).toBe('Test Conversation'); + expect(savedConvo?.title).toBe('Test Conversation'); }); it('should query messages when saving a conversation', async () => { // Mock messages as ObjectIds - const mongoose = require('mongoose'); const mockMessages = [new mongoose.Types.ObjectId(), new mongoose.Types.ObjectId()]; getMessages.mockResolvedValue(mockMessages); - await saveConvo(mockReq, mockConversationData); + await saveConvo(mockCtx, mockConversationData); // Verify that getMessages was called with correct parameters expect(getMessages).toHaveBeenCalledWith( @@ -98,18 +142,18 @@ describe('Conversation Operations', () => { it('should handle newConversationId when provided', async () => { const newConversationId = uuidv4(); - const result = await saveConvo(mockReq, { + const result = await saveConvo(mockCtx, { ...mockConversationData, newConversationId, }); - expect(result.conversationId).toBe(newConversationId); + expect(result?.conversationId).toBe(newConversationId); }); it('should not create a conversation when noUpsert is true and conversation does not exist', async () => { const nonExistentId = uuidv4(); const result = await saveConvo( - mockReq, + mockCtx, { conversationId: nonExistentId, title: 'Ghost Title' }, { noUpsert: true }, ); @@ -121,30 +165,30 @@ describe('Conversation Operations', () => { }); it('should update an existing conversation when noUpsert is true', async () => { - await saveConvo(mockReq, mockConversationData); + await saveConvo(mockCtx, mockConversationData); const result = await saveConvo( - mockReq, + mockCtx, { conversationId: mockConversationData.conversationId, title: 'Updated Title' }, { noUpsert: true }, ); expect(result).not.toBeNull(); - expect(result.title).toBe('Updated Title'); - expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result?.title).toBe('Updated Title'); + expect(result?.conversationId).toBe(mockConversationData.conversationId); }); it('should still upsert by default when noUpsert is not provided', async () => { const newId = uuidv4(); - const result = await saveConvo(mockReq, { + const result = await saveConvo(mockCtx, { conversationId: newId, title: 'New Conversation', endpoint: EModelEndpoint.openAI, }); expect(result).not.toBeNull(); - expect(result.conversationId).toBe(newId); - expect(result.title).toBe('New Conversation'); + expect(result?.conversationId).toBe(newId); + expect(result?.title).toBe('New Conversation'); }); it('should handle unsetFields metadata', async () => { @@ -152,31 +196,30 @@ describe('Conversation Operations', () => { unsetFields: { someField: 1 }, }; - await saveConvo(mockReq, mockConversationData, metadata); + await saveConvo(mockCtx, mockConversationData, metadata); - const savedConvo = await Conversation.findOne({ + const savedConvo = await Conversation.findOne({ conversationId: mockConversationData.conversationId, }); - expect(savedConvo.someField).toBeUndefined(); + expect(savedConvo?.someField).toBeUndefined(); }); }); describe('isTemporary conversation handling', () => { it('should save a conversation with expiredAt when isTemporary is true', async () => { - mockReq.config.interfaceConfig.temporaryChatRetention = 24; - - mockReq.body = { isTemporary: true }; + mockCtx.interfaceConfig = { temporaryChatRetention: 24 }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); const afterSave = new Date(); - expect(result.conversationId).toBe(mockConversationData.conversationId); - expect(result.expiredAt).toBeDefined(); - expect(result.expiredAt).toBeInstanceOf(Date); + expect(result?.conversationId).toBe(mockConversationData.conversationId); + expect(result?.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeInstanceOf(Date); const expectedExpirationTime = new Date(beforeSave.getTime() + 24 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -187,36 +230,35 @@ describe('Conversation Operations', () => { }); it('should save a conversation without expiredAt when isTemporary is false', async () => { - mockReq.body = { isTemporary: false }; + mockCtx.isTemporary = false; - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); - expect(result.conversationId).toBe(mockConversationData.conversationId); - expect(result.expiredAt).toBeNull(); + expect(result?.conversationId).toBe(mockConversationData.conversationId); + expect(result?.expiredAt).toBeNull(); }); it('should save a conversation without expiredAt when isTemporary is not provided', async () => { - mockReq.body = {}; + mockCtx.isTemporary = undefined; - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); - expect(result.conversationId).toBe(mockConversationData.conversationId); - expect(result.expiredAt).toBeNull(); + expect(result?.conversationId).toBe(mockConversationData.conversationId); + expect(result?.expiredAt).toBeNull(); }); it('should use custom retention period from config', async () => { - mockReq.config.interfaceConfig.temporaryChatRetention = 48; - - mockReq.body = { isTemporary: true }; + mockCtx.interfaceConfig = { temporaryChatRetention: 48 }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Verify expiredAt is approximately 48 hours in the future const expectedExpirationTime = new Date(beforeSave.getTime() + 48 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -228,18 +270,17 @@ describe('Conversation Operations', () => { it('should handle minimum retention period (1 hour)', async () => { // Mock app config with less than minimum retention - mockReq.config.interfaceConfig.temporaryChatRetention = 0.5; // Half hour - should be clamped to 1 hour - - mockReq.body = { isTemporary: true }; + mockCtx.interfaceConfig = { temporaryChatRetention: 0.5 }; // Half hour - should be clamped to 1 hour + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Verify expiredAt is approximately 1 hour in the future (minimum) const expectedExpirationTime = new Date(beforeSave.getTime() + 1 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -251,18 +292,17 @@ describe('Conversation Operations', () => { it('should handle maximum retention period (8760 hours)', async () => { // Mock app config with more than maximum retention - mockReq.config.interfaceConfig.temporaryChatRetention = 10000; // Should be clamped to 8760 hours - - mockReq.body = { isTemporary: true }; + mockCtx.interfaceConfig = { temporaryChatRetention: 10000 }; // Should be clamped to 8760 hours + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Verify expiredAt is approximately 8760 hours (1 year) in the future const expectedExpirationTime = new Date(beforeSave.getTime() + 8760 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -274,22 +314,21 @@ describe('Conversation Operations', () => { it('should handle missing config gracefully', async () => { // Simulate missing config - should use default retention period - delete mockReq.config; - - mockReq.body = { isTemporary: true }; + mockCtx.interfaceConfig = undefined; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); const afterSave = new Date(); // Should still save the conversation with default retention period (30 days) - expect(result.conversationId).toBe(mockConversationData.conversationId); - expect(result.expiredAt).toBeDefined(); - expect(result.expiredAt).toBeInstanceOf(Date); + expect(result?.conversationId).toBe(mockConversationData.conversationId); + expect(result?.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeInstanceOf(Date); // Verify expiredAt is approximately 30 days in the future (720 hours) const expectedExpirationTime = new Date(beforeSave.getTime() + 720 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -301,18 +340,17 @@ describe('Conversation Operations', () => { it('should use default retention when config is not provided', async () => { // Mock getAppConfig to return empty config - mockReq.config = {}; // Empty config - - mockReq.body = { isTemporary: true }; + mockCtx.interfaceConfig = undefined; // Empty config + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Default retention is 30 days (720 hours) const expectedExpirationTime = new Date(beforeSave.getTime() + 30 * 24 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -324,40 +362,39 @@ describe('Conversation Operations', () => { it('should update expiredAt when saving existing temporary conversation', async () => { // First save a temporary conversation - mockReq.config.interfaceConfig.temporaryChatRetention = 24; - - mockReq.body = { isTemporary: true }; - const firstSave = await saveConvo(mockReq, mockConversationData); - const originalExpiredAt = firstSave.expiredAt; + mockCtx.interfaceConfig = { temporaryChatRetention: 24 }; + mockCtx.isTemporary = true; + const firstSave = await saveConvo(mockCtx, mockConversationData); + const originalExpiredAt = firstSave?.expiredAt ?? new Date(0); // Wait a bit to ensure time difference await new Promise((resolve) => setTimeout(resolve, 100)); // Save again with same conversationId but different title const updatedData = { ...mockConversationData, title: 'Updated Title' }; - const secondSave = await saveConvo(mockReq, updatedData); + const secondSave = await saveConvo(mockCtx, updatedData); // Should update title and create new expiredAt - expect(secondSave.title).toBe('Updated Title'); - expect(secondSave.expiredAt).toBeDefined(); - expect(new Date(secondSave.expiredAt).getTime()).toBeGreaterThan( + expect(secondSave?.title).toBe('Updated Title'); + expect(secondSave?.expiredAt).toBeDefined(); + expect(new Date(secondSave?.expiredAt ?? 0).getTime()).toBeGreaterThan( new Date(originalExpiredAt).getTime(), ); }); it('should not set expiredAt when updating non-temporary conversation', async () => { // First save a non-temporary conversation - mockReq.body = { isTemporary: false }; - const firstSave = await saveConvo(mockReq, mockConversationData); - expect(firstSave.expiredAt).toBeNull(); + mockCtx.isTemporary = false; + const firstSave = await saveConvo(mockCtx, mockConversationData); + expect(firstSave?.expiredAt).toBeNull(); // Update without isTemporary flag - mockReq.body = {}; + mockCtx.isTemporary = undefined; const updatedData = { ...mockConversationData, title: 'Updated Title' }; - const secondSave = await saveConvo(mockReq, updatedData); + const secondSave = await saveConvo(mockCtx, updatedData); - expect(secondSave.title).toBe('Updated Title'); - expect(secondSave.expiredAt).toBeNull(); + expect(secondSave?.title).toBe('Updated Title'); + expect(secondSave?.expiredAt).toBeNull(); }); it('should filter out expired conversations in getConvosByCursor', async () => { @@ -381,13 +418,13 @@ describe('Conversation Operations', () => { }); // Mock Meili search - Conversation.meiliSearch = jest.fn().mockResolvedValue({ hits: [] }); + Object.assign(Conversation, { meiliSearch: jest.fn().mockResolvedValue({ hits: [] }) }); const result = await getConvosByCursor('user123'); // Should only return conversations with null or non-existent expiredAt - expect(result.conversations).toHaveLength(1); - expect(result.conversations[0].conversationId).toBe(nonExpiredConvo.conversationId); + expect(result?.conversations).toHaveLength(1); + expect(result?.conversations[0]?.conversationId).toBe(nonExpiredConvo.conversationId); }); it('should filter out expired conversations in getConvosQueried', async () => { @@ -416,10 +453,10 @@ describe('Conversation Operations', () => { const result = await getConvosQueried('user123', convoIds); // Should only return the non-expired conversation - expect(result.conversations).toHaveLength(1); - expect(result.conversations[0].conversationId).toBe(nonExpiredConvo.conversationId); - expect(result.convoMap[nonExpiredConvo.conversationId]).toBeDefined(); - expect(result.convoMap[expiredConvo.conversationId]).toBeUndefined(); + expect(result?.conversations).toHaveLength(1); + expect(result?.conversations[0].conversationId).toBe(nonExpiredConvo.conversationId); + expect(result?.convoMap[nonExpiredConvo.conversationId]).toBeDefined(); + expect(result?.convoMap[expiredConvo.conversationId]).toBeUndefined(); }); }); @@ -435,9 +472,9 @@ describe('Conversation Operations', () => { const result = await searchConversation(mockConversationData.conversationId); expect(result).toBeTruthy(); - expect(result.conversationId).toBe(mockConversationData.conversationId); - expect(result.user).toBe('user123'); - expect(result.title).toBeUndefined(); // Only returns conversationId and user + expect(result!.conversationId).toBe(mockConversationData.conversationId); + expect(result!.user).toBe('user123'); + expect((result as unknown as { title?: string }).title).toBeUndefined(); // Only returns conversationId and user }); it('should return null if conversation not found', async () => { @@ -457,9 +494,9 @@ describe('Conversation Operations', () => { const result = await getConvo('user123', mockConversationData.conversationId); - expect(result.conversationId).toBe(mockConversationData.conversationId); - expect(result.user).toBe('user123'); - expect(result.title).toBe('Test Conversation'); + expect(result!.conversationId).toBe(mockConversationData.conversationId); + expect(result!.user).toBe('user123'); + expect(result!.title).toBe('Test Conversation'); }); it('should return null if conversation not found', async () => { @@ -545,8 +582,8 @@ describe('Conversation Operations', () => { conversationId: mockConversationData.conversationId, }); - expect(result.deletedCount).toBe(1); - expect(result.messages.deletedCount).toBe(5); + expect(result?.deletedCount).toBe(1); + expect(result?.messages.deletedCount).toBe(5); expect(deleteMessages).toHaveBeenCalledWith({ conversationId: { $in: [mockConversationData.conversationId] }, user: 'user123', @@ -582,8 +619,8 @@ describe('Conversation Operations', () => { const result = await deleteNullOrEmptyConversations(); - expect(result.conversations.deletedCount).toBe(0); // No invalid conversations to delete - expect(result.messages.deletedCount).toBe(0); + expect(result?.conversations.deletedCount).toBe(0); // No invalid conversations to delete + expect(result?.messages.deletedCount).toBe(0); // Verify valid conversation remains const remainingConvos = await Conversation.find({}); @@ -597,7 +634,7 @@ describe('Conversation Operations', () => { // Force a database error by disconnecting await mongoose.disconnect(); - const result = await saveConvo(mockReq, mockConversationData); + const result = await saveConvo(mockCtx, mockConversationData); expect(result).toEqual({ message: 'Error saving conversation' }); @@ -611,7 +648,7 @@ describe('Conversation Operations', () => { * Helper to create conversations with specific timestamps * Uses collection.insertOne to bypass Mongoose timestamps entirely */ - const createConvoWithTimestamps = async (index, createdAt, updatedAt) => { + const createConvoWithTimestamps = async (index: number, createdAt: Date, updatedAt: Date) => { const conversationId = uuidv4(); // Use collection-level insert to bypass Mongoose timestamps await Conversation.collection.insertOne({ @@ -630,7 +667,7 @@ describe('Conversation Operations', () => { it('should not skip conversations at page boundaries', async () => { // Create 30 conversations to ensure pagination (limit is 25) const baseTime = new Date('2026-01-01T00:00:00.000Z'); - const convos = []; + const convos: unknown[] = []; for (let i = 0; i < 30; i++) { const updatedAt = new Date(baseTime.getTime() - i * 60000); // Each 1 minute apart @@ -656,8 +693,8 @@ describe('Conversation Operations', () => { // Verify no duplicates and no gaps const allIds = [ - ...page1.conversations.map((c) => c.conversationId), - ...page2.conversations.map((c) => c.conversationId), + ...page1.conversations.map((c: IConversation) => c.conversationId), + ...page2.conversations.map((c: IConversation) => c.conversationId), ]; const uniqueIds = new Set(allIds); @@ -672,7 +709,7 @@ describe('Conversation Operations', () => { const baseTime = new Date('2026-01-01T12:00:00.000Z'); // Create exactly 26 conversations - const convos = []; + const convos: (IConversation | null)[] = []; for (let i = 0; i < 26; i++) { const updatedAt = new Date(baseTime.getTime() - i * 60000); const convo = await createConvoWithTimestamps(i, updatedAt, updatedAt); @@ -689,8 +726,8 @@ describe('Conversation Operations', () => { expect(page1.nextCursor).toBeTruthy(); // Item 26 should NOT be in page 1 - const page1Ids = page1.conversations.map((c) => c.conversationId); - expect(page1Ids).not.toContain(item26.conversationId); + const page1Ids = page1.conversations.map((c: IConversation) => c.conversationId); + expect(page1Ids).not.toContain(item26!.conversationId); // Fetch second page const page2 = await getConvosByCursor('user123', { @@ -700,7 +737,7 @@ describe('Conversation Operations', () => { // Item 26 MUST be in page 2 (this was the bug - it was being skipped) expect(page2.conversations).toHaveLength(1); - expect(page2.conversations[0].conversationId).toBe(item26.conversationId); + expect(page2.conversations[0].conversationId).toBe(item26!.conversationId); }); it('should sort by updatedAt DESC by default', async () => { @@ -727,10 +764,10 @@ describe('Conversation Operations', () => { const result = await getConvosByCursor('user123'); // Should be sorted by updatedAt DESC (most recent first) - expect(result.conversations).toHaveLength(3); - expect(result.conversations[0].conversationId).toBe(convo1.conversationId); // Jan 3 updatedAt - expect(result.conversations[1].conversationId).toBe(convo2.conversationId); // Jan 2 updatedAt - expect(result.conversations[2].conversationId).toBe(convo3.conversationId); // Jan 1 updatedAt + expect(result?.conversations).toHaveLength(3); + expect(result?.conversations[0].conversationId).toBe(convo1!.conversationId); // Jan 3 updatedAt + expect(result?.conversations[1].conversationId).toBe(convo2!.conversationId); // Jan 2 updatedAt + expect(result?.conversations[2].conversationId).toBe(convo3!.conversationId); // Jan 1 updatedAt }); it('should handle conversations with same updatedAt (tie-breaker)', async () => { @@ -744,12 +781,12 @@ describe('Conversation Operations', () => { const result = await getConvosByCursor('user123'); // All 3 should be returned (no skipping due to same timestamps) - expect(result.conversations).toHaveLength(3); + expect(result?.conversations).toHaveLength(3); - const returnedIds = result.conversations.map((c) => c.conversationId); - expect(returnedIds).toContain(convo1.conversationId); - expect(returnedIds).toContain(convo2.conversationId); - expect(returnedIds).toContain(convo3.conversationId); + const returnedIds = result?.conversations.map((c: IConversation) => c.conversationId); + expect(returnedIds).toContain(convo1!.conversationId); + expect(returnedIds).toContain(convo2!.conversationId); + expect(returnedIds).toContain(convo3!.conversationId); }); it('should handle cursor pagination with conversations updated during pagination', async () => { @@ -806,13 +843,15 @@ describe('Conversation Operations', () => { const page1 = await getConvosByCursor('user123', { limit: 25 }); // Decode the cursor to verify it's based on the last RETURNED item - const decodedCursor = JSON.parse(Buffer.from(page1.nextCursor, 'base64').toString()); + const decodedCursor = JSON.parse( + Buffer.from(page1.nextCursor as string, 'base64').toString(), + ); // The cursor should match the last item in page1 (item at index 24) - const lastReturnedItem = page1.conversations[24]; + const lastReturnedItem = page1.conversations[24] as IConversation; expect(new Date(decodedCursor.primary).getTime()).toBe( - new Date(lastReturnedItem.updatedAt).getTime(), + new Date(lastReturnedItem.updatedAt ?? 0).getTime(), ); }); @@ -831,26 +870,26 @@ describe('Conversation Operations', () => { ); // Verify timestamps were set correctly - expect(new Date(convo1.createdAt).getTime()).toBe( + expect(new Date(convo1!.createdAt ?? 0).getTime()).toBe( new Date('2026-01-03T00:00:00.000Z').getTime(), ); - expect(new Date(convo2.createdAt).getTime()).toBe( + expect(new Date(convo2!.createdAt ?? 0).getTime()).toBe( new Date('2026-01-01T00:00:00.000Z').getTime(), ); const result = await getConvosByCursor('user123', { sortBy: 'createdAt' }); // Should be sorted by createdAt DESC - expect(result.conversations).toHaveLength(2); - expect(result.conversations[0].conversationId).toBe(convo1.conversationId); // Jan 3 createdAt - expect(result.conversations[1].conversationId).toBe(convo2.conversationId); // Jan 1 createdAt + expect(result?.conversations).toHaveLength(2); + expect(result?.conversations[0].conversationId).toBe(convo1!.conversationId); // Jan 3 createdAt + expect(result?.conversations[1].conversationId).toBe(convo2!.conversationId); // Jan 1 createdAt }); it('should handle empty result set gracefully', async () => { const result = await getConvosByCursor('user123'); - expect(result.conversations).toHaveLength(0); - expect(result.nextCursor).toBeNull(); + expect(result?.conversations).toHaveLength(0); + expect(result?.nextCursor).toBeNull(); }); it('should handle exactly limit number of conversations (no next page)', async () => { @@ -864,8 +903,8 @@ describe('Conversation Operations', () => { const result = await getConvosByCursor('user123', { limit: 25 }); - expect(result.conversations).toHaveLength(25); - expect(result.nextCursor).toBeNull(); // No next page + expect(result?.conversations).toHaveLength(25); + expect(result?.nextCursor).toBeNull(); // No next page }); }); }); diff --git a/packages/data-schemas/src/methods/conversation.ts b/packages/data-schemas/src/methods/conversation.ts new file mode 100644 index 0000000000..7a62afef9e --- /dev/null +++ b/packages/data-schemas/src/methods/conversation.ts @@ -0,0 +1,488 @@ +import type { FilterQuery, Model, SortOrder } from 'mongoose'; +import logger from '~/config/winston'; +import { createTempChatExpirationDate } from '~/utils/tempChatRetention'; +import type { AppConfig, IConversation } from '~/types'; +import type { MessageMethods } from './message'; +import type { DeleteResult } from 'mongoose'; + +export interface ConversationMethods { + getConvoFiles(conversationId: string): Promise; + searchConversation(conversationId: string): Promise; + deleteNullOrEmptyConversations(): Promise<{ + conversations: { deletedCount?: number }; + messages: { deletedCount?: number }; + }>; + saveConvo( + ctx: { userId: string; isTemporary?: boolean; interfaceConfig?: AppConfig['interfaceConfig'] }, + data: { conversationId: string; newConversationId?: string; [key: string]: unknown }, + metadata?: { context?: string; unsetFields?: Record; noUpsert?: boolean }, + ): Promise; + bulkSaveConvos(conversations: Array>): Promise; + getConvosByCursor( + user: string, + options?: { + cursor?: string | null; + limit?: number; + isArchived?: boolean; + tags?: string[]; + search?: string; + sortBy?: string; + sortDirection?: string; + }, + ): Promise<{ conversations: IConversation[]; nextCursor: string | null }>; + getConvosQueried( + user: string, + convoIds: Array<{ conversationId: string }> | null, + cursor?: string | null, + limit?: number, + ): Promise<{ + conversations: IConversation[]; + nextCursor: string | null; + convoMap: Record; + }>; + getConvo(user: string, conversationId: string): Promise; + getConvoTitle(user: string, conversationId: string): Promise; + deleteConvos( + user: string, + filter: FilterQuery, + ): Promise; +} + +export function createConversationMethods( + mongoose: typeof import('mongoose'), + messageMethods?: Pick, +): ConversationMethods { + function getMessageMethods() { + if (!messageMethods) { + throw new Error('Message methods not injected into conversation methods'); + } + return messageMethods; + } + + /** + * Searches for a conversation by conversationId and returns a lean document with only conversationId and user. + */ + async function searchConversation(conversationId: string) { + try { + const Conversation = mongoose.models.Conversation as Model; + return await Conversation.findOne({ conversationId }, 'conversationId user').lean(); + } catch (error) { + logger.error('[searchConversation] Error searching conversation', error); + throw new Error('Error searching conversation'); + } + } + + /** + * Retrieves a single conversation for a given user and conversation ID. + */ + async function getConvo(user: string, conversationId: string) { + try { + const Conversation = mongoose.models.Conversation as Model; + return await Conversation.findOne({ user, conversationId }).lean(); + } catch (error) { + logger.error('[getConvo] Error getting single conversation', error); + throw new Error('Error getting single conversation'); + } + } + + /** + * Deletes conversations and messages with null or empty IDs. + */ + async function deleteNullOrEmptyConversations() { + try { + const Conversation = mongoose.models.Conversation as Model; + const { deleteMessages } = getMessageMethods(); + const filter = { + $or: [ + { conversationId: null }, + { conversationId: '' }, + { conversationId: { $exists: false } }, + ], + }; + + const result = await Conversation.deleteMany(filter); + const messageDeleteResult = await deleteMessages(filter); + + logger.info( + `[deleteNullOrEmptyConversations] Deleted ${result.deletedCount} conversations and ${messageDeleteResult.deletedCount} messages`, + ); + + return { + conversations: result, + messages: messageDeleteResult, + }; + } catch (error) { + logger.error('[deleteNullOrEmptyConversations] Error deleting conversations', error); + throw new Error('Error deleting conversations with null or empty conversationId'); + } + } + + /** + * Searches for a conversation by conversationId and returns associated file ids. + */ + async function getConvoFiles(conversationId: string): Promise { + try { + const Conversation = mongoose.models.Conversation as Model; + return ( + ((await Conversation.findOne({ conversationId }, 'files').lean()) as IConversation | null) + ?.files ?? [] + ); + } catch (error) { + logger.error('[getConvoFiles] Error getting conversation files', error); + throw new Error('Error getting conversation files'); + } + } + + /** + * Saves a conversation to the database. + */ + async function saveConvo( + { + userId, + isTemporary, + interfaceConfig, + }: { + userId: string; + isTemporary?: boolean; + interfaceConfig?: AppConfig['interfaceConfig']; + }, + { + conversationId, + newConversationId, + ...convo + }: { + conversationId: string; + newConversationId?: string; + [key: string]: unknown; + }, + metadata?: { context?: string; unsetFields?: Record; noUpsert?: boolean }, + ) { + try { + const Conversation = mongoose.models.Conversation as Model; + const { getMessages } = getMessageMethods(); + + if (metadata?.context) { + logger.debug(`[saveConvo] ${metadata.context}`); + } + + const messages = await getMessages({ conversationId }, '_id'); + const update: Record = { ...convo, messages, user: userId }; + + if (newConversationId) { + update.conversationId = newConversationId; + } + + if (isTemporary) { + try { + update.expiredAt = createTempChatExpirationDate(interfaceConfig); + } catch (err) { + logger.error('Error creating temporary chat expiration date:', err); + logger.info(`---\`saveConvo\` context: ${metadata?.context}`); + update.expiredAt = null; + } + } else { + update.expiredAt = null; + } + + const updateOperation: Record = { $set: update }; + if (metadata?.unsetFields && Object.keys(metadata.unsetFields).length > 0) { + updateOperation.$unset = metadata.unsetFields; + } + + const conversation = await Conversation.findOneAndUpdate( + { conversationId, user: userId }, + updateOperation, + { + new: true, + upsert: metadata?.noUpsert !== true, + }, + ); + + if (!conversation) { + logger.debug('[saveConvo] Conversation not found, skipping update'); + return null; + } + + return conversation.toObject(); + } catch (error) { + logger.error('[saveConvo] Error saving conversation', error); + if (metadata?.context) { + logger.info(`[saveConvo] ${metadata.context}`); + } + return { message: 'Error saving conversation' }; + } + } + + /** + * Saves multiple conversations in bulk. + */ + async function bulkSaveConvos(conversations: Array>) { + try { + const Conversation = mongoose.models.Conversation as Model; + const bulkOps = conversations.map((convo) => ({ + updateOne: { + filter: { conversationId: convo.conversationId, user: convo.user }, + update: convo, + upsert: true, + timestamps: false, + }, + })); + + const result = await Conversation.bulkWrite(bulkOps); + return result; + } catch (error) { + logger.error('[bulkSaveConvos] Error saving conversations in bulk', error); + throw new Error('Failed to save conversations in bulk.'); + } + } + + /** + * Retrieves conversations using cursor-based pagination. + */ + async function getConvosByCursor( + user: string, + { + cursor, + limit = 25, + isArchived = false, + tags, + search, + sortBy = 'updatedAt', + sortDirection = 'desc', + }: { + cursor?: string | null; + limit?: number; + isArchived?: boolean; + tags?: string[]; + search?: string; + sortBy?: string; + sortDirection?: string; + } = {}, + ) { + const Conversation = mongoose.models.Conversation as Model; + const filters: FilterQuery[] = [{ user } as FilterQuery]; + if (isArchived) { + filters.push({ isArchived: true } as FilterQuery); + } else { + filters.push({ + $or: [{ isArchived: false }, { isArchived: { $exists: false } }], + } as FilterQuery); + } + + if (Array.isArray(tags) && tags.length > 0) { + filters.push({ tags: { $in: tags } } as FilterQuery); + } + + filters.push({ + $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }], + } as FilterQuery); + + if (search) { + try { + const meiliResults = await ( + Conversation as unknown as { + meiliSearch: ( + query: string, + options: Record, + ) => Promise<{ + hits: Array<{ conversationId: string }>; + }>; + } + ).meiliSearch(search, { filter: `user = "${user}"` }); + const matchingIds = Array.isArray(meiliResults.hits) + ? meiliResults.hits.map((result) => result.conversationId) + : []; + if (!matchingIds.length) { + return { conversations: [], nextCursor: null }; + } + filters.push({ conversationId: { $in: matchingIds } } as FilterQuery); + } catch (error) { + logger.error('[getConvosByCursor] Error during meiliSearch', error); + throw new Error('Error during meiliSearch'); + } + } + + const validSortFields = ['title', 'createdAt', 'updatedAt']; + if (!validSortFields.includes(sortBy)) { + throw new Error( + `Invalid sortBy field: ${sortBy}. Must be one of ${validSortFields.join(', ')}`, + ); + } + const finalSortBy = sortBy; + const finalSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; + + let cursorFilter: FilterQuery | null = null; + if (cursor) { + try { + const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString()); + const { primary, secondary } = decoded; + const primaryValue = finalSortBy === 'title' ? primary : new Date(primary); + const secondaryValue = new Date(secondary); + const op = finalSortDirection === 'asc' ? '$gt' : '$lt'; + + cursorFilter = { + $or: [ + { [finalSortBy]: { [op]: primaryValue } }, + { + [finalSortBy]: primaryValue, + updatedAt: { [op]: secondaryValue }, + }, + ], + } as FilterQuery; + } catch { + logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning'); + } + if (cursorFilter) { + filters.push(cursorFilter); + } + } + + const query: FilterQuery = + filters.length === 1 ? filters[0] : ({ $and: filters } as FilterQuery); + + try { + const sortOrder: SortOrder = finalSortDirection === 'asc' ? 1 : -1; + const sortObj: Record = { [finalSortBy]: sortOrder }; + + if (finalSortBy !== 'updatedAt') { + sortObj.updatedAt = sortOrder; + } + + const convos = await Conversation.find(query) + .select( + 'conversationId endpoint title createdAt updatedAt user model agent_id assistant_id spec iconURL', + ) + .sort(sortObj) + .limit(limit + 1) + .lean(); + + let nextCursor: string | null = null; + if (convos.length > limit) { + convos.pop(); + const lastReturned = convos[convos.length - 1] as Record; + const primaryValue = lastReturned[finalSortBy]; + const primaryStr = + finalSortBy === 'title' ? primaryValue : (primaryValue as Date).toISOString(); + const secondaryStr = (lastReturned.updatedAt as Date).toISOString(); + const composite = { primary: primaryStr, secondary: secondaryStr }; + nextCursor = Buffer.from(JSON.stringify(composite)).toString('base64'); + } + + return { conversations: convos, nextCursor }; + } catch (error) { + logger.error('[getConvosByCursor] Error getting conversations', error); + throw new Error('Error getting conversations'); + } + } + + /** + * Fetches specific conversations by ID array with pagination. + */ + async function getConvosQueried( + user: string, + convoIds: Array<{ conversationId: string }> | null, + cursor: string | null = null, + limit = 25, + ) { + try { + const Conversation = mongoose.models.Conversation as Model; + if (!convoIds?.length) { + return { conversations: [], nextCursor: null, convoMap: {} }; + } + + const conversationIds = convoIds.map((convo) => convo.conversationId); + + const results = await Conversation.find({ + user, + conversationId: { $in: conversationIds }, + $or: [{ expiredAt: { $exists: false } }, { expiredAt: null }], + }).lean(); + + results.sort( + (a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime(), + ); + + let filtered = results; + if (cursor && cursor !== 'start') { + const cursorDate = new Date(cursor); + filtered = results.filter((convo) => new Date(convo.updatedAt ?? 0) < cursorDate); + } + + const limited = filtered.slice(0, limit + 1); + let nextCursor: string | null = null; + if (limited.length > limit) { + limited.pop(); + nextCursor = (limited[limited.length - 1].updatedAt as Date).toISOString(); + } + + const convoMap: Record = {}; + limited.forEach((convo) => { + convoMap[convo.conversationId] = convo; + }); + + return { conversations: limited, nextCursor, convoMap }; + } catch (error) { + logger.error('[getConvosQueried] Error getting conversations', error); + throw new Error('Error fetching conversations'); + } + } + + /** + * Gets conversation title, returning 'New Chat' as default. + */ + async function getConvoTitle(user: string, conversationId: string) { + try { + const convo = await getConvo(user, conversationId); + if (convo && !convo.title) { + return null; + } else { + return convo?.title || 'New Chat'; + } + } catch (error) { + logger.error('[getConvoTitle] Error getting conversation title', error); + throw new Error('Error getting conversation title'); + } + } + + /** + * Deletes conversations and their associated messages for a given user and filter. + */ + async function deleteConvos(user: string, filter: FilterQuery) { + try { + const Conversation = mongoose.models.Conversation as Model; + const { deleteMessages } = getMessageMethods(); + const userFilter = { ...filter, user }; + const conversations = await Conversation.find(userFilter).select('conversationId'); + const conversationIds = conversations.map((c) => c.conversationId); + + if (!conversationIds.length) { + throw new Error('Conversation not found or already deleted.'); + } + + const deleteConvoResult = await Conversation.deleteMany(userFilter); + + const deleteMessagesResult = await deleteMessages({ + conversationId: { $in: conversationIds }, + user, + }); + + return { ...deleteConvoResult, messages: deleteMessagesResult }; + } catch (error) { + logger.error('[deleteConvos] Error deleting conversations and messages', error); + throw error; + } + } + + return { + getConvoFiles, + searchConversation, + deleteNullOrEmptyConversations, + saveConvo, + bulkSaveConvos, + getConvosByCursor, + getConvosQueried, + getConvo, + getConvoTitle, + deleteConvos, + }; +} diff --git a/api/models/ConversationTag.spec.js b/packages/data-schemas/src/methods/conversationTag.methods.spec.ts similarity index 73% rename from api/models/ConversationTag.spec.js rename to packages/data-schemas/src/methods/conversationTag.methods.spec.ts index bc7da919e1..0b4c6268d6 100644 --- a/api/models/ConversationTag.spec.js +++ b/packages/data-schemas/src/methods/conversationTag.methods.spec.ts @@ -1,13 +1,38 @@ -const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { ConversationTag, Conversation } = require('~/db/models'); -const { deleteConversationTag } = require('./ConversationTag'); +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { createConversationTagMethods } from './conversationTag'; +import { createModels } from '~/models'; +import type { IConversationTag } from '~/schema/conversationTag'; +import type { IConversation } from '..'; -let mongoServer; +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +})); + +let mongoServer: InstanceType; +let ConversationTag: mongoose.Model; +let Conversation: mongoose.Model; +let deleteConversationTag: ReturnType['deleteConversationTag']; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); - await mongoose.connect(mongoServer.getUri()); + const mongoUri = mongoServer.getUri(); + + // Register models + const models = createModels(mongoose); + Object.assign(mongoose.models, models); + + ConversationTag = mongoose.models.ConversationTag; + Conversation = mongoose.models.Conversation; + + // Create methods from factory + const methods = createConversationTagMethods(mongoose); + deleteConversationTag = methods.deleteConversationTag; + + await mongoose.connect(mongoUri); }); afterAll(async () => { @@ -47,7 +72,7 @@ describe('ConversationTag model - $pullAll operations', () => { const result = await deleteConversationTag(userId, 'temp'); expect(result).toBeDefined(); - expect(result.tag).toBe('temp'); + expect(result!.tag).toBe('temp'); const remaining = await ConversationTag.find({ user: userId }).lean(); expect(remaining).toHaveLength(0); @@ -91,8 +116,8 @@ describe('ConversationTag model - $pullAll operations', () => { const myConvo = await Conversation.findOne({ conversationId: 'mine' }).lean(); const theirConvo = await Conversation.findOne({ conversationId: 'theirs' }).lean(); - expect(myConvo.tags).toEqual([]); - expect(theirConvo.tags).toEqual(['shared-name']); + expect(myConvo?.tags).toEqual([]); + expect(theirConvo?.tags).toEqual(['shared-name']); }); it('should handle duplicate tags in conversations correctly', async () => { @@ -108,7 +133,7 @@ describe('ConversationTag model - $pullAll operations', () => { await deleteConversationTag(userId, 'dup'); const updated = await Conversation.findById(conv._id).lean(); - expect(updated.tags).toEqual(['other']); + expect(updated?.tags).toEqual(['other']); }); }); }); diff --git a/packages/data-schemas/src/methods/conversationTag.ts b/packages/data-schemas/src/methods/conversationTag.ts new file mode 100644 index 0000000000..af1e43babb --- /dev/null +++ b/packages/data-schemas/src/methods/conversationTag.ts @@ -0,0 +1,312 @@ +import type { Model } from 'mongoose'; +import logger from '~/config/winston'; + +interface IConversationTag { + user: string; + tag: string; + description?: string; + position: number; + count: number; + createdAt?: Date; + [key: string]: unknown; +} + +export function createConversationTagMethods(mongoose: typeof import('mongoose')) { + /** + * Retrieves all conversation tags for a user. + */ + async function getConversationTags(user: string) { + try { + const ConversationTag = mongoose.models.ConversationTag as Model; + return await ConversationTag.find({ user }).sort({ position: 1 }).lean(); + } catch (error) { + logger.error('[getConversationTags] Error getting conversation tags', error); + throw new Error('Error getting conversation tags'); + } + } + + /** + * Creates a new conversation tag. + */ + async function createConversationTag( + user: string, + data: { + tag: string; + description?: string; + addToConversation?: boolean; + conversationId?: string; + }, + ) { + try { + const ConversationTag = mongoose.models.ConversationTag as Model; + const Conversation = mongoose.models.Conversation; + const { tag, description, addToConversation, conversationId } = data; + + const existingTag = await ConversationTag.findOne({ user, tag }).lean(); + if (existingTag) { + return existingTag; + } + + const maxPosition = await ConversationTag.findOne({ user }).sort('-position').lean(); + const position = (maxPosition?.position || 0) + 1; + + const newTag = await ConversationTag.findOneAndUpdate( + { tag, user }, + { + tag, + user, + count: addToConversation ? 1 : 0, + position, + description, + $setOnInsert: { createdAt: new Date() }, + }, + { + new: true, + upsert: true, + lean: true, + }, + ); + + if (addToConversation && conversationId) { + await Conversation.findOneAndUpdate( + { user, conversationId }, + { $addToSet: { tags: tag } }, + { new: true }, + ); + } + + return newTag; + } catch (error) { + logger.error('[createConversationTag] Error creating conversation tag', error); + throw new Error('Error creating conversation tag'); + } + } + + /** + * Adjusts positions of tags when a tag's position is changed. + */ + async function adjustPositions(user: string, oldPosition: number, newPosition: number) { + if (oldPosition === newPosition) { + return; + } + + const ConversationTag = mongoose.models.ConversationTag as Model; + + const update = + oldPosition < newPosition ? { $inc: { position: -1 } } : { $inc: { position: 1 } }; + const position = + oldPosition < newPosition + ? { + $gt: Math.min(oldPosition, newPosition), + $lte: Math.max(oldPosition, newPosition), + } + : { + $gte: Math.min(oldPosition, newPosition), + $lt: Math.max(oldPosition, newPosition), + }; + + await ConversationTag.updateMany({ user, position }, update); + } + + /** + * Updates an existing conversation tag. + */ + async function updateConversationTag( + user: string, + oldTag: string, + data: { tag?: string; description?: string; position?: number }, + ) { + try { + const ConversationTag = mongoose.models.ConversationTag as Model; + const Conversation = mongoose.models.Conversation; + const { tag: newTag, description, position } = data; + + const existingTag = await ConversationTag.findOne({ user, tag: oldTag }).lean(); + if (!existingTag) { + return null; + } + + if (newTag && newTag !== oldTag) { + const tagAlreadyExists = await ConversationTag.findOne({ user, tag: newTag }).lean(); + if (tagAlreadyExists) { + throw new Error('Tag already exists'); + } + + await Conversation.updateMany({ user, tags: oldTag }, { $set: { 'tags.$': newTag } }); + } + + const updateData: Record = {}; + if (newTag) { + updateData.tag = newTag; + } + if (description !== undefined) { + updateData.description = description; + } + if (position !== undefined) { + await adjustPositions(user, existingTag.position, position); + updateData.position = position; + } + + return await ConversationTag.findOneAndUpdate({ user, tag: oldTag }, updateData, { + new: true, + lean: true, + }); + } catch (error) { + logger.error('[updateConversationTag] Error updating conversation tag', error); + throw new Error('Error updating conversation tag'); + } + } + + /** + * Deletes a conversation tag. + */ + async function deleteConversationTag(user: string, tag: string) { + try { + const ConversationTag = mongoose.models.ConversationTag as Model; + const Conversation = mongoose.models.Conversation; + + const deletedTag = await ConversationTag.findOneAndDelete({ user, tag }).lean(); + if (!deletedTag) { + return null; + } + + await Conversation.updateMany({ user, tags: tag }, { $pullAll: { tags: [tag] } }); + + await ConversationTag.updateMany( + { user, position: { $gt: deletedTag.position } }, + { $inc: { position: -1 } }, + ); + + return deletedTag; + } catch (error) { + logger.error('[deleteConversationTag] Error deleting conversation tag', error); + throw new Error('Error deleting conversation tag'); + } + } + + /** + * Updates tags for a specific conversation. + */ + async function updateTagsForConversation(user: string, conversationId: string, tags: string[]) { + try { + const ConversationTag = mongoose.models.ConversationTag as Model; + const Conversation = mongoose.models.Conversation; + + const conversation = await Conversation.findOne({ user, conversationId }).lean(); + if (!conversation) { + throw new Error('Conversation not found'); + } + + const oldTags = new Set( + ((conversation as Record).tags as string[]) ?? [], + ); + const newTags = new Set(tags); + + const addedTags = [...newTags].filter((tag) => !oldTags.has(tag)); + const removedTags = [...oldTags].filter((tag) => !newTags.has(tag)); + + const bulkOps: Array<{ + updateOne: { + filter: Record; + update: Record; + upsert?: boolean; + }; + }> = []; + + for (const tag of addedTags) { + bulkOps.push({ + updateOne: { + filter: { user, tag }, + update: { $inc: { count: 1 } }, + upsert: true, + }, + }); + } + + for (const tag of removedTags) { + bulkOps.push({ + updateOne: { + filter: { user, tag }, + update: { $inc: { count: -1 } }, + }, + }); + } + + if (bulkOps.length > 0) { + await ConversationTag.bulkWrite(bulkOps); + } + + const updatedConversation = ( + await Conversation.findOneAndUpdate( + { user, conversationId }, + { $set: { tags: [...newTags] } }, + { new: true }, + ) + ).toObject(); + + return updatedConversation.tags; + } catch (error) { + logger.error('[updateTagsForConversation] Error updating tags', error); + throw new Error('Error updating tags for conversation'); + } + } + + /** + * Increments tag counts for existing tags only. + */ + async function bulkIncrementTagCounts(user: string, tags: string[]) { + if (!tags || tags.length === 0) { + return; + } + + try { + const ConversationTag = mongoose.models.ConversationTag as Model; + const uniqueTags = [...new Set(tags.filter(Boolean))]; + if (uniqueTags.length === 0) { + return; + } + + const bulkOps = uniqueTags.map((tag) => ({ + updateOne: { + filter: { user, tag }, + update: { $inc: { count: 1 } }, + }, + })); + + const result = await ConversationTag.bulkWrite(bulkOps); + if (result && result.modifiedCount > 0) { + logger.debug( + `user: ${user} | Incremented tag counts - modified ${result.modifiedCount} tags`, + ); + } + } catch (error) { + logger.error('[bulkIncrementTagCounts] Error incrementing tag counts', error); + } + } + + /** + * Deletes all conversation tags matching the given filter. + */ + async function deleteConversationTags(filter: Record): Promise { + try { + const ConversationTag = mongoose.models.ConversationTag as Model; + const result = await ConversationTag.deleteMany(filter); + return result.deletedCount; + } catch (error) { + logger.error('[deleteConversationTags] Error deleting conversation tags', error); + throw new Error('Error deleting conversation tags'); + } + } + + return { + getConversationTags, + createConversationTag, + updateConversationTag, + deleteConversationTag, + deleteConversationTags, + bulkIncrementTagCounts, + updateTagsForConversation, + }; +} + +export type ConversationTagMethods = ReturnType; diff --git a/api/models/convoStructure.spec.js b/packages/data-schemas/src/methods/convoStructure.spec.ts similarity index 69% rename from api/models/convoStructure.spec.js rename to packages/data-schemas/src/methods/convoStructure.spec.ts index 440f21cb06..77a9913233 100644 --- a/api/models/convoStructure.spec.js +++ b/packages/data-schemas/src/methods/convoStructure.spec.ts @@ -1,13 +1,35 @@ -const mongoose = require('mongoose'); -const { buildTree } = require('librechat-data-provider'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { getMessages, bulkSaveMessages } = require('./Message'); -const { Message } = require('~/db/models'); +import mongoose from 'mongoose'; +import type { TMessage } from 'librechat-data-provider'; +import { buildTree } from 'librechat-data-provider'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { createModels } from '~/models'; +import { createMessageMethods } from './message'; +import type { IMessage } from '..'; + +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +})); + +let mongod: InstanceType; +let Message: mongoose.Model; +let getMessages: ReturnType['getMessages']; +let bulkSaveMessages: ReturnType['bulkSaveMessages']; -let mongod; beforeAll(async () => { mongod = await MongoMemoryServer.create(); const uri = mongod.getUri(); + + const models = createModels(mongoose); + Object.assign(mongoose.models, models); + Message = mongoose.models.Message; + + const methods = createMessageMethods(mongoose); + getMessages = methods.getMessages; + bulkSaveMessages = methods.bulkSaveMessages; + await mongoose.connect(uri); }); @@ -61,11 +83,13 @@ describe('Conversation Structure Tests', () => { // Add common properties to all messages messages.forEach((msg) => { - msg.conversationId = conversationId; - msg.user = userId; - msg.isCreatedByUser = false; - msg.error = false; - msg.unfinished = false; + Object.assign(msg, { + conversationId, + user: userId, + isCreatedByUser: false, + error: false, + unfinished: false, + }); }); // Save messages with overrideTimestamp omitted (default is false) @@ -75,10 +99,10 @@ describe('Conversation Structure Tests', () => { const retrievedMessages = await getMessages({ conversationId, user: userId }); // Build tree - const tree = buildTree({ messages: retrievedMessages }); + const tree = buildTree({ messages: retrievedMessages as TMessage[] }); // Check if the tree is incorrect (folded/corrupted) - expect(tree.length).toBeGreaterThan(1); // Should have multiple root messages, indicating corruption + expect(tree!.length).toBeGreaterThan(1); // Should have multiple root messages, indicating corruption }); test('Fix: Conversation structure maintained with more than 16 messages', async () => { @@ -102,17 +126,17 @@ describe('Conversation Structure Tests', () => { const retrievedMessages = await getMessages({ conversationId, user: userId }); // Build tree - const tree = buildTree({ messages: retrievedMessages }); + const tree = buildTree({ messages: retrievedMessages as TMessage[] }); // Check if the tree is correct - expect(tree.length).toBe(1); // Should have only one root message - let currentNode = tree[0]; + expect(tree!.length).toBe(1); // Should have only one root message + let currentNode = tree![0]; for (let i = 1; i < 20; i++) { - expect(currentNode.children.length).toBe(1); - currentNode = currentNode.children[0]; + expect(currentNode.children!.length).toBe(1); + currentNode = currentNode.children![0]; expect(currentNode.text).toBe(`Message ${i}`); } - expect(currentNode.children.length).toBe(0); // Last message should have no children + expect(currentNode.children!.length).toBe(0); // Last message should have no children }); test('Simulate MongoDB ordering issue with more than 16 messages and close timestamps', async () => { @@ -131,15 +155,13 @@ describe('Conversation Structure Tests', () => { // Add common properties to all messages messages.forEach((msg) => { - msg.isCreatedByUser = false; - msg.error = false; - msg.unfinished = false; + Object.assign(msg, { isCreatedByUser: false, error: false, unfinished: false }); }); await bulkSaveMessages(messages, true); const retrievedMessages = await getMessages({ conversationId, user: userId }); - const tree = buildTree({ messages: retrievedMessages }); - expect(tree.length).toBeGreaterThan(1); + const tree = buildTree({ messages: retrievedMessages as TMessage[] }); + expect(tree!.length).toBeGreaterThan(1); }); test('Fix: Preserve order with more than 16 messages by maintaining original timestamps', async () => { @@ -158,9 +180,7 @@ describe('Conversation Structure Tests', () => { // Add common properties to all messages messages.forEach((msg) => { - msg.isCreatedByUser = false; - msg.error = false; - msg.unfinished = false; + Object.assign(msg, { isCreatedByUser: false, error: false, unfinished: false }); }); // Save messages with overriding timestamps (preserve original timestamps) @@ -170,17 +190,17 @@ describe('Conversation Structure Tests', () => { const retrievedMessages = await getMessages({ conversationId, user: userId }); // Build tree - const tree = buildTree({ messages: retrievedMessages }); + const tree = buildTree({ messages: retrievedMessages as TMessage[] }); // Check if the tree is correct - expect(tree.length).toBe(1); // Should have only one root message - let currentNode = tree[0]; + expect(tree!.length).toBe(1); // Should have only one root message + let currentNode = tree![0]; for (let i = 1; i < 20; i++) { - expect(currentNode.children.length).toBe(1); - currentNode = currentNode.children[0]; + expect(currentNode.children!.length).toBe(1); + currentNode = currentNode.children![0]; expect(currentNode.text).toBe(`Message ${i}`); } - expect(currentNode.children.length).toBe(0); // Last message should have no children + expect(currentNode.children!.length).toBe(0); // Last message should have no children }); test('Random order dates between parent and children messages', async () => { @@ -217,11 +237,13 @@ describe('Conversation Structure Tests', () => { // Add common properties to all messages messages.forEach((msg) => { - msg.conversationId = conversationId; - msg.user = userId; - msg.isCreatedByUser = false; - msg.error = false; - msg.unfinished = false; + Object.assign(msg, { + conversationId, + user: userId, + isCreatedByUser: false, + error: false, + unfinished: false, + }); }); // Save messages with overrideTimestamp set to true @@ -241,16 +263,16 @@ describe('Conversation Structure Tests', () => { ); // Build tree - const tree = buildTree({ messages: retrievedMessages }); + const tree = buildTree({ messages: retrievedMessages as TMessage[] }); // Debug log to see the tree structure console.log( 'Tree structure:', - tree.map((root) => ({ + tree!.map((root) => ({ messageId: root.messageId, - children: root.children.map((child) => ({ + children: root.children!.map((child) => ({ messageId: child.messageId, - children: child.children.map((grandchild) => ({ + children: child.children!.map((grandchild) => ({ messageId: grandchild.messageId, })), })), @@ -262,14 +284,14 @@ describe('Conversation Structure Tests', () => { // Check if messages are properly linked const parentMsg = retrievedMessages.find((msg) => msg.messageId === 'parent'); - expect(parentMsg.parentMessageId).toBeNull(); // Parent should have null parentMessageId + expect(parentMsg!.parentMessageId).toBeNull(); // Parent should have null parentMessageId const childMsg1 = retrievedMessages.find((msg) => msg.messageId === 'child1'); - expect(childMsg1.parentMessageId).toBe('parent'); + expect(childMsg1!.parentMessageId).toBe('parent'); // Then check tree structure - expect(tree.length).toBe(1); // Should have only one root message - expect(tree[0].messageId).toBe('parent'); - expect(tree[0].children.length).toBe(2); // Should have two children + expect(tree!.length).toBe(1); // Should have only one root message + expect(tree![0].messageId).toBe('parent'); + expect(tree![0].children!.length).toBe(2); // Should have two children }); }); diff --git a/packages/data-schemas/src/methods/file.acl.spec.ts b/packages/data-schemas/src/methods/file.acl.spec.ts new file mode 100644 index 0000000000..240b535bd8 --- /dev/null +++ b/packages/data-schemas/src/methods/file.acl.spec.ts @@ -0,0 +1,405 @@ +import mongoose from 'mongoose'; +import { v4 as uuidv4 } from 'uuid'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + ResourceType, + AccessRoleIds, + PrincipalType, + PermissionBits, +} from 'librechat-data-provider'; +import type { AccessRole as TAccessRole, AclEntry as TAclEntry } from '..'; +import type { Types } from 'mongoose'; +import { createAclEntryMethods } from './aclEntry'; +import { createModels } from '../models'; +import { createMethods } from './index'; + +/** Lean access role object from .lean() */ +type LeanAccessRole = TAccessRole & { _id: mongoose.Types.ObjectId }; + +/** Lean ACL entry from .lean() */ +type LeanAclEntry = TAclEntry & { _id: mongoose.Types.ObjectId }; + +/** Tool resources shape for agent file access */ +type AgentToolResources = { + file_search?: { file_ids?: string[] }; + code_interpreter?: { file_ids?: string[] }; +}; + +let File: mongoose.Model; +let Agent: mongoose.Model; +let AclEntry: mongoose.Model; +let AccessRole: mongoose.Model; +let User: mongoose.Model; +let methods: ReturnType; +let aclMethods: ReturnType; + +describe('File Access Control', () => { + let mongoServer: MongoMemoryServer; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + + createModels(mongoose); + File = mongoose.models.File; + Agent = mongoose.models.Agent; + AclEntry = mongoose.models.AclEntry; + AccessRole = mongoose.models.AccessRole; + User = mongoose.models.User; + + methods = createMethods(mongoose); + aclMethods = createAclEntryMethods(mongoose); + + // Seed default access roles + await methods.seedDefaultRoles(); + }); + + afterAll(async () => { + const collections = mongoose.connection.collections; + for (const key in collections) { + await collections[key].deleteMany({}); + } + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await File.deleteMany({}); + await Agent.deleteMany({}); + await AclEntry.deleteMany({}); + await User.deleteMany({}); + }); + + describe('File ACL entry operations', () => { + it('should create ACL entries for agent file access', async () => { + const userId = new mongoose.Types.ObjectId(); + const authorId = new mongoose.Types.ObjectId(); + const agentId = uuidv4(); + const fileIds = [uuidv4(), uuidv4(), uuidv4(), uuidv4()]; + + // Create users + await User.create({ + _id: userId, + email: 'user@example.com', + emailVerified: true, + provider: 'local', + }); + + await User.create({ + _id: authorId, + email: 'author@example.com', + emailVerified: true, + provider: 'local', + }); + + // Create files + for (const fileId of fileIds) { + await methods.createFile({ + user: authorId, + file_id: fileId, + filename: `file-${fileId}.txt`, + filepath: `/uploads/${fileId}`, + }); + } + + // Create agent with only first two files attached + const agent = await methods.createAgent({ + id: agentId, + name: 'Test Agent', + author: authorId, + model: 'gpt-4', + provider: 'openai', + tool_resources: { + file_search: { + file_ids: [fileIds[0], fileIds[1]], + }, + }, + }); + + // Grant EDIT permission to user on the agent + const editorRole = (await AccessRole.findOne({ + accessRoleId: AccessRoleIds.AGENT_EDITOR, + }).lean()) as LeanAccessRole | null; + + if (editorRole) { + await aclMethods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + editorRole.permBits, + authorId, + undefined, + editorRole._id, + ); + } + + // Verify ACL entry exists for the user + const aclEntry = (await AclEntry.findOne({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + }).lean()) as LeanAclEntry | null; + + expect(aclEntry).toBeTruthy(); + + // Check that agent has correct file_ids in tool_resources + const agentRecord = await methods.getAgent({ id: agentId }); + const toolResources = agentRecord?.tool_resources as AgentToolResources | undefined; + expect(toolResources?.file_search?.file_ids).toContain(fileIds[0]); + expect(toolResources?.file_search?.file_ids).toContain(fileIds[1]); + expect(toolResources?.file_search?.file_ids).not.toContain(fileIds[2]); + expect(toolResources?.file_search?.file_ids).not.toContain(fileIds[3]); + }); + + it('should grant access to agent author via ACL', async () => { + const authorId = new mongoose.Types.ObjectId(); + const agentId = uuidv4(); + + await User.create({ + _id: authorId, + email: 'author@example.com', + emailVerified: true, + provider: 'local', + }); + + const agent = await methods.createAgent({ + id: agentId, + name: 'Test Agent', + author: authorId, + model: 'gpt-4', + provider: 'openai', + }); + + // Grant owner permissions + const ownerRole = (await AccessRole.findOne({ + accessRoleId: AccessRoleIds.AGENT_OWNER, + }).lean()) as LeanAccessRole | null; + + if (ownerRole) { + await aclMethods.grantPermission( + PrincipalType.USER, + authorId, + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + ownerRole.permBits, + authorId, + undefined, + ownerRole._id, + ); + } + + // Author should have full permission bits on the agent + const hasView = await aclMethods.hasPermission( + [{ principalType: PrincipalType.USER, principalId: authorId }], + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + PermissionBits.VIEW, + ); + + const hasEdit = await aclMethods.hasPermission( + [{ principalType: PrincipalType.USER, principalId: authorId }], + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + PermissionBits.EDIT, + ); + + expect(hasView).toBe(true); + expect(hasEdit).toBe(true); + }); + + it('should deny access when no ACL entry exists', async () => { + const userId = new mongoose.Types.ObjectId(); + const agentId = new mongoose.Types.ObjectId(); + + const hasAccess = await aclMethods.hasPermission( + [{ principalType: PrincipalType.USER, principalId: userId }], + ResourceType.AGENT, + agentId, + PermissionBits.VIEW, + ); + + expect(hasAccess).toBe(false); + }); + + it('should deny EDIT when user only has VIEW permission', async () => { + const userId = new mongoose.Types.ObjectId(); + const authorId = new mongoose.Types.ObjectId(); + const agentId = uuidv4(); + + await User.create({ + _id: userId, + email: 'user@example.com', + emailVerified: true, + provider: 'local', + }); + + await User.create({ + _id: authorId, + email: 'author@example.com', + emailVerified: true, + provider: 'local', + }); + + const agent = await methods.createAgent({ + id: agentId, + name: 'View-Only Agent', + author: authorId, + model: 'gpt-4', + provider: 'openai', + }); + + // Grant only VIEW permission + const viewerRole = (await AccessRole.findOne({ + accessRoleId: AccessRoleIds.AGENT_VIEWER, + }).lean()) as LeanAccessRole | null; + + if (viewerRole) { + await aclMethods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + viewerRole.permBits, + authorId, + undefined, + viewerRole._id, + ); + } + + const canView = await aclMethods.hasPermission( + [{ principalType: PrincipalType.USER, principalId: userId }], + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + PermissionBits.VIEW, + ); + + const canEdit = await aclMethods.hasPermission( + [{ principalType: PrincipalType.USER, principalId: userId }], + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + PermissionBits.EDIT, + ); + + expect(canView).toBe(true); + expect(canEdit).toBe(false); + }); + + it('should support role-based permission grants', async () => { + const userId = new mongoose.Types.ObjectId(); + const authorId = new mongoose.Types.ObjectId(); + const agentId = uuidv4(); + + await User.create({ + _id: userId, + email: 'user@example.com', + emailVerified: true, + provider: 'local', + role: 'ADMIN', + }); + + await User.create({ + _id: authorId, + email: 'author@example.com', + emailVerified: true, + provider: 'local', + }); + + const agent = await methods.createAgent({ + id: agentId, + name: 'Test Agent', + author: authorId, + model: 'gpt-4', + provider: 'openai', + }); + + // Grant permission to ADMIN role + const editorRole = (await AccessRole.findOne({ + accessRoleId: AccessRoleIds.AGENT_EDITOR, + }).lean()) as LeanAccessRole | null; + + if (editorRole) { + await aclMethods.grantPermission( + PrincipalType.ROLE, + 'ADMIN', + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + editorRole.permBits, + authorId, + undefined, + editorRole._id, + ); + } + + // User with ADMIN role should have access through role-based ACL + const hasAccess = await aclMethods.hasPermission( + [ + { principalType: PrincipalType.USER, principalId: userId }, + { + principalType: PrincipalType.ROLE, + principalId: 'ADMIN' as unknown as mongoose.Types.ObjectId, + }, + ], + ResourceType.AGENT, + agent._id as string | Types.ObjectId, + PermissionBits.VIEW, + ); + + expect(hasAccess).toBe(true); + }); + }); + + describe('getFiles with file queries', () => { + it('should return files created by user', async () => { + const userId = new mongoose.Types.ObjectId(); + const fileId1 = `file_${uuidv4()}`; + const fileId2 = `file_${uuidv4()}`; + + await methods.createFile({ + file_id: fileId1, + user: userId, + filename: 'file1.txt', + filepath: '/uploads/file1.txt', + type: 'text/plain', + bytes: 100, + }); + + await methods.createFile({ + file_id: fileId2, + user: new mongoose.Types.ObjectId(), + filename: 'file2.txt', + filepath: '/uploads/file2.txt', + type: 'text/plain', + bytes: 200, + }); + + const files = await methods.getFiles({ file_id: { $in: [fileId1, fileId2] } }); + expect(files).toHaveLength(2); + }); + + it('should return all files matching query', async () => { + const userId = new mongoose.Types.ObjectId(); + const fileId1 = `file_${uuidv4()}`; + const fileId2 = `file_${uuidv4()}`; + + await methods.createFile({ + file_id: fileId1, + user: userId, + filename: 'file1.txt', + filepath: '/uploads/file1.txt', + }); + + await methods.createFile({ + file_id: fileId2, + user: userId, + filename: 'file2.txt', + filepath: '/uploads/file2.txt', + }); + + const files = await methods.getFiles({ user: userId }); + expect(files).toHaveLength(2); + }); + }); +}); diff --git a/packages/data-schemas/src/methods/index.ts b/packages/data-schemas/src/methods/index.ts index 07e7cefc24..6246d74343 100644 --- a/packages/data-schemas/src/methods/index.ts +++ b/packages/data-schemas/src/methods/index.ts @@ -1,6 +1,6 @@ import { createSessionMethods, DEFAULT_REFRESH_TOKEN_EXPIRY, type SessionMethods } from './session'; import { createTokenMethods, type TokenMethods } from './token'; -import { createRoleMethods, type RoleMethods } from './role'; +import { createRoleMethods, type RoleMethods, type RoleDeps } from './role'; import { createUserMethods, DEFAULT_SESSION_EXPIRY, type UserMethods } from './user'; export { DEFAULT_REFRESH_TOKEN_EXPIRY, DEFAULT_SESSION_EXPIRY }; @@ -21,7 +21,34 @@ import { createAccessRoleMethods, type AccessRoleMethods } from './accessRole'; import { createUserGroupMethods, type UserGroupMethods } from './userGroup'; import { createAclEntryMethods, type AclEntryMethods } from './aclEntry'; import { createShareMethods, type ShareMethods } from './share'; +/* Tier 1 — Simple CRUD */ +import { createActionMethods, type ActionMethods } from './action'; +import { createAssistantMethods, type AssistantMethods } from './assistant'; +import { createBannerMethods, type BannerMethods } from './banner'; +import { createToolCallMethods, type ToolCallMethods } from './toolCall'; +import { createCategoriesMethods, type CategoriesMethods } from './categories'; +import { createPresetMethods, type PresetMethods } from './preset'; +/* Tier 2 — Moderate (service deps injected) */ +import { createConversationTagMethods, type ConversationTagMethods } from './conversationTag'; +import { createMessageMethods, type MessageMethods } from './message'; +import { createConversationMethods, type ConversationMethods } from './conversation'; +/* Tier 3 — Complex (heavier injection) */ +import { + createTxMethods, + type TxMethods, + type TxDeps, + tokenValues, + cacheTokenValues, + premiumTokenValues, + defaultRate, +} from './tx'; import { createTransactionMethods, type TransactionMethods } from './transaction'; +import { createSpendTokensMethods, type SpendTokensMethods } from './spendTokens'; +import { createPromptMethods, type PromptMethods, type PromptDeps } from './prompt'; +/* Tier 5 — Agent */ +import { createAgentMethods, type AgentMethods, type AgentDeps } from './agent'; + +export { tokenValues, cacheTokenValues, premiumTokenValues, defaultRate }; export type AllMethods = UserMethods & SessionMethods & @@ -38,18 +65,105 @@ export type AllMethods = UserMethods & ShareMethods & AccessRoleMethods & PluginAuthMethods & - TransactionMethods; + ActionMethods & + AssistantMethods & + BannerMethods & + ToolCallMethods & + CategoriesMethods & + PresetMethods & + ConversationTagMethods & + MessageMethods & + ConversationMethods & + TxMethods & + TransactionMethods & + SpendTokensMethods & + PromptMethods & + AgentMethods; + +/** Dependencies injected from the api layer into createMethods */ +export interface CreateMethodsDeps { + /** Matches a model name to a canonical key. From @librechat/api. */ + matchModelName?: (model: string, endpoint?: string) => string | undefined; + /** Finds the first key in values whose key is a substring of model. From @librechat/api. */ + findMatchingPattern?: (model: string, values: Record) => string | undefined; + /** Removes all ACL permissions for a resource. From PermissionService. */ + removeAllPermissions?: (params: { resourceType: string; resourceId: unknown }) => Promise; + /** Returns a cache store for the given key. From getLogStores. */ + getCache?: RoleDeps['getCache']; +} /** * Creates all database methods for all collections * @param mongoose - Mongoose instance + * @param deps - Optional dependencies injected from the api layer */ -export function createMethods(mongoose: typeof import('mongoose')): AllMethods { +export function createMethods( + mongoose: typeof import('mongoose'), + deps: CreateMethodsDeps = {}, +): AllMethods { + // Tier 3: tx methods need matchModelName and findMatchingPattern + const txDeps: TxDeps = { + matchModelName: deps.matchModelName ?? (() => undefined), + findMatchingPattern: deps.findMatchingPattern ?? (() => undefined), + }; + const txMethods = createTxMethods(mongoose, txDeps); + + // Tier 3: transaction methods need tx's getMultiplier/getCacheMultiplier + const transactionMethods = createTransactionMethods(mongoose, { + getMultiplier: txMethods.getMultiplier, + getCacheMultiplier: txMethods.getCacheMultiplier, + }); + + // Tier 3: spendTokens methods need transaction methods + const spendTokensMethods = createSpendTokensMethods(mongoose, { + createTransaction: transactionMethods.createTransaction, + createStructuredTransaction: transactionMethods.createStructuredTransaction, + }); + + const messageMethods = createMessageMethods(mongoose); + + const conversationMethods = createConversationMethods(mongoose, { + getMessages: messageMethods.getMessages, + deleteMessages: messageMethods.deleteMessages, + }); + + // ACL entry methods (used internally for removeAllPermissions) + const aclEntryMethods = createAclEntryMethods(mongoose); + + // Internal removeAllPermissions: use deleteAclEntries from aclEntryMethods + // instead of requiring it as an external dep from PermissionService + const removeAllPermissions = + deps.removeAllPermissions ?? + (async ({ resourceType, resourceId }: { resourceType: string; resourceId: unknown }) => { + await aclEntryMethods.deleteAclEntries({ resourceType, resourceId }); + }); + + const promptDeps: PromptDeps = { + removeAllPermissions, + getSoleOwnedResourceIds: aclEntryMethods.getSoleOwnedResourceIds, + }; + const promptMethods = createPromptMethods(mongoose, promptDeps); + + // Role methods with optional cache injection + const roleDeps: RoleDeps = { getCache: deps.getCache }; + const roleMethods = createRoleMethods(mongoose, roleDeps); + + // Tier 1: action methods (created as variable for agent dependency) + const actionMethods = createActionMethods(mongoose); + + // Tier 5: agent methods need removeAllPermissions + getActions + const agentDeps: AgentDeps = { + removeAllPermissions, + getActions: actionMethods.getActions, + getSoleOwnedResourceIds: aclEntryMethods.getSoleOwnedResourceIds, + }; + const agentMethods = createAgentMethods(mongoose, agentDeps); + return { ...createUserMethods(mongoose), ...createSessionMethods(mongoose), ...createTokenMethods(mongoose), - ...createRoleMethods(mongoose), + ...roleMethods, ...createKeyMethods(mongoose), ...createFileMethods(mongoose), ...createMemoryMethods(mongoose), @@ -58,10 +172,27 @@ export function createMethods(mongoose: typeof import('mongoose')): AllMethods { ...createMCPServerMethods(mongoose), ...createAccessRoleMethods(mongoose), ...createUserGroupMethods(mongoose), - ...createAclEntryMethods(mongoose), + ...aclEntryMethods, ...createShareMethods(mongoose), ...createPluginAuthMethods(mongoose), - ...createTransactionMethods(mongoose), + /* Tier 1 */ + ...actionMethods, + ...createAssistantMethods(mongoose), + ...createBannerMethods(mongoose), + ...createToolCallMethods(mongoose), + ...createCategoriesMethods(mongoose), + ...createPresetMethods(mongoose), + /* Tier 2 */ + ...createConversationTagMethods(mongoose), + ...messageMethods, + ...conversationMethods, + /* Tier 3 */ + ...txMethods, + ...transactionMethods, + ...spendTokensMethods, + ...promptMethods, + /* Tier 5 */ + ...agentMethods, }; } @@ -81,5 +212,18 @@ export type { ShareMethods, AccessRoleMethods, PluginAuthMethods, + ActionMethods, + AssistantMethods, + BannerMethods, + ToolCallMethods, + CategoriesMethods, + PresetMethods, + ConversationTagMethods, + MessageMethods, + ConversationMethods, + TxMethods, TransactionMethods, + SpendTokensMethods, + PromptMethods, + AgentMethods, }; diff --git a/packages/data-schemas/src/methods/memory.ts b/packages/data-schemas/src/methods/memory.ts index becb063f3d..749fbc9cf1 100644 --- a/packages/data-schemas/src/methods/memory.ts +++ b/packages/data-schemas/src/methods/memory.ts @@ -158,12 +158,28 @@ export function createMemoryMethods(mongoose: typeof import('mongoose')) { } } + /** + * Deletes all memory entries for a user + */ + async function deleteAllUserMemories(userId: string | Types.ObjectId): Promise { + try { + const MemoryEntry = mongoose.models.MemoryEntry; + const result = await MemoryEntry.deleteMany({ userId }); + return result.deletedCount; + } catch (error) { + throw new Error( + `Failed to delete all user memories: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } + } + return { setMemory, createMemory, deleteMemory, getAllUserMemories, getFormattedMemories, + deleteAllUserMemories, }; } diff --git a/api/models/Message.spec.js b/packages/data-schemas/src/methods/message.spec.ts similarity index 67% rename from api/models/Message.spec.js rename to packages/data-schemas/src/methods/message.spec.ts index 39b5b4337c..ac85a035b7 100644 --- a/api/models/Message.spec.js +++ b/packages/data-schemas/src/methods/message.spec.ts @@ -1,52 +1,73 @@ -const mongoose = require('mongoose'); -const { v4: uuidv4 } = require('uuid'); -const { messageSchema } = require('@librechat/data-schemas'); -const { MongoMemoryServer } = require('mongodb-memory-server'); +import mongoose from 'mongoose'; +import { v4 as uuidv4 } from 'uuid'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import type { IMessage } from '..'; +import { createMessageMethods } from './message'; +import { createModels } from '../models'; -const { - saveMessage, - getMessages, - updateMessage, - deleteMessages, - bulkSaveMessages, - updateMessageText, - deleteMessagesSince, -} = require('./Message'); +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +})); -jest.mock('~/server/services/Config/app'); +let mongoServer: InstanceType; +let Message: mongoose.Model; +let saveMessage: ReturnType['saveMessage']; +let getMessages: ReturnType['getMessages']; +let updateMessage: ReturnType['updateMessage']; +let deleteMessages: ReturnType['deleteMessages']; +let bulkSaveMessages: ReturnType['bulkSaveMessages']; +let updateMessageText: ReturnType['updateMessageText']; +let deleteMessagesSince: ReturnType['deleteMessagesSince']; -/** - * @type {import('mongoose').Model} - */ -let Message; +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + + const models = createModels(mongoose); + Object.assign(mongoose.models, models); + Message = mongoose.models.Message; + + const methods = createMessageMethods(mongoose); + saveMessage = methods.saveMessage; + getMessages = methods.getMessages; + updateMessage = methods.updateMessage; + deleteMessages = methods.deleteMessages; + bulkSaveMessages = methods.bulkSaveMessages; + updateMessageText = methods.updateMessageText; + deleteMessagesSince = methods.deleteMessagesSince; + + await mongoose.connect(mongoUri); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); describe('Message Operations', () => { - let mongoServer; - let mockReq; - let mockMessageData; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - Message = mongoose.models.Message || mongoose.model('Message', messageSchema); - await mongoose.connect(mongoUri); - }); - - afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - }); + let mockCtx: { + userId: string; + isTemporary?: boolean; + interfaceConfig?: { temporaryChatRetention?: number }; + }; + let mockMessageData: Partial = { + messageId: 'msg123', + conversationId: uuidv4(), + text: 'Hello, world!', + user: 'user123', + }; beforeEach(async () => { // Clear database await Message.deleteMany({}); - mockReq = { - user: { id: 'user123' }, - config: { - interfaceConfig: { - temporaryChatRetention: 24, // Default 24 hours - }, + mockCtx = { + userId: 'user123', + interfaceConfig: { + temporaryChatRetention: 24, // Default 24 hours }, }; @@ -60,26 +81,26 @@ describe('Message Operations', () => { describe('saveMessage', () => { it('should save a message for an authenticated user', async () => { - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); - expect(result.messageId).toBe('msg123'); - expect(result.user).toBe('user123'); - expect(result.text).toBe('Hello, world!'); + expect(result?.messageId).toBe('msg123'); + expect(result?.user).toBe('user123'); + expect(result?.text).toBe('Hello, world!'); // Verify the message was actually saved to the database const savedMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); expect(savedMessage).toBeTruthy(); - expect(savedMessage.text).toBe('Hello, world!'); + expect(savedMessage?.text).toBe('Hello, world!'); }); it('should throw an error for unauthenticated user', async () => { - mockReq.user = null; - await expect(saveMessage(mockReq, mockMessageData)).rejects.toThrow('User not authenticated'); + mockCtx.userId = null as unknown as string; + await expect(saveMessage(mockCtx, mockMessageData)).rejects.toThrow('User not authenticated'); }); it('should handle invalid conversation ID gracefully', async () => { mockMessageData.conversationId = 'invalid-id'; - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); expect(result).toBeUndefined(); }); }); @@ -87,35 +108,38 @@ describe('Message Operations', () => { describe('updateMessageText', () => { it('should update message text for the authenticated user', async () => { // First save a message - await saveMessage(mockReq, mockMessageData); + await saveMessage(mockCtx, mockMessageData); // Then update it - await updateMessageText(mockReq, { messageId: 'msg123', text: 'Updated text' }); + await updateMessageText(mockCtx.userId, { messageId: 'msg123', text: 'Updated text' }); // Verify the update const updatedMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); - expect(updatedMessage.text).toBe('Updated text'); + expect(updatedMessage?.text).toBe('Updated text'); }); }); describe('updateMessage', () => { it('should update a message for the authenticated user', async () => { // First save a message - await saveMessage(mockReq, mockMessageData); + await saveMessage(mockCtx, mockMessageData); - const result = await updateMessage(mockReq, { messageId: 'msg123', text: 'Updated text' }); + const result = await updateMessage(mockCtx.userId, { + messageId: 'msg123', + text: 'Updated text', + }); - expect(result.messageId).toBe('msg123'); - expect(result.text).toBe('Updated text'); + expect(result?.messageId).toBe('msg123'); + expect(result?.text).toBe('Updated text'); // Verify in database const updatedMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); - expect(updatedMessage.text).toBe('Updated text'); + expect(updatedMessage?.text).toBe('Updated text'); }); it('should throw an error if message is not found', async () => { await expect( - updateMessage(mockReq, { messageId: 'nonexistent', text: 'Test' }), + updateMessage(mockCtx.userId, { messageId: 'nonexistent', text: 'Test' }), ).rejects.toThrow('Message not found or user not authorized.'); }); }); @@ -125,21 +149,21 @@ describe('Message Operations', () => { const conversationId = uuidv4(); // Create multiple messages in the same conversation - await saveMessage(mockReq, { + await saveMessage(mockCtx, { messageId: 'msg1', conversationId, text: 'First message', user: 'user123', }); - await saveMessage(mockReq, { + await saveMessage(mockCtx, { messageId: 'msg2', conversationId, text: 'Second message', user: 'user123', }); - await saveMessage(mockReq, { + await saveMessage(mockCtx, { messageId: 'msg3', conversationId, text: 'Third message', @@ -147,7 +171,7 @@ describe('Message Operations', () => { }); // Delete messages since message2 (this should only delete messages created AFTER msg2) - await deleteMessagesSince(mockReq, { + await deleteMessagesSince(mockCtx.userId, { messageId: 'msg2', conversationId, }); @@ -161,7 +185,7 @@ describe('Message Operations', () => { }); it('should return undefined if no message is found', async () => { - const result = await deleteMessagesSince(mockReq, { + const result = await deleteMessagesSince(mockCtx.userId, { messageId: 'nonexistent', conversationId: 'convo123', }); @@ -174,14 +198,14 @@ describe('Message Operations', () => { const conversationId = uuidv4(); // Save some messages - await saveMessage(mockReq, { + await saveMessage(mockCtx, { messageId: 'msg1', conversationId, text: 'First message', user: 'user123', }); - await saveMessage(mockReq, { + await saveMessage(mockCtx, { messageId: 'msg2', conversationId, text: 'Second message', @@ -198,9 +222,9 @@ describe('Message Operations', () => { describe('deleteMessages', () => { it('should delete messages with the correct filter', async () => { // Save some messages for different users - await saveMessage(mockReq, mockMessageData); + await saveMessage(mockCtx, mockMessageData); await saveMessage( - { user: { id: 'user456' } }, + { userId: 'user456' }, { messageId: 'msg456', conversationId: uuidv4(), @@ -222,22 +246,23 @@ describe('Message Operations', () => { describe('Conversation Hijacking Prevention', () => { it("should not allow editing a message in another user's conversation", async () => { - const attackerReq = { user: { id: 'attacker123' } }; const victimConversationId = uuidv4(); const victimMessageId = 'victim-msg-123'; // First, save a message as the victim (but we'll try to edit as attacker) - const victimReq = { user: { id: 'victim123' } }; - await saveMessage(victimReq, { - messageId: victimMessageId, - conversationId: victimConversationId, - text: 'Victim message', - user: 'victim123', - }); + await saveMessage( + { userId: 'victim123' }, + { + messageId: victimMessageId, + conversationId: victimConversationId, + text: 'Victim message', + user: 'victim123', + }, + ); // Attacker tries to edit the victim's message await expect( - updateMessage(attackerReq, { + updateMessage('attacker123', { messageId: victimMessageId, conversationId: victimConversationId, text: 'Hacked message', @@ -249,25 +274,26 @@ describe('Message Operations', () => { messageId: victimMessageId, user: 'victim123', }); - expect(originalMessage.text).toBe('Victim message'); + expect(originalMessage?.text).toBe('Victim message'); }); it("should not allow deleting messages from another user's conversation", async () => { - const attackerReq = { user: { id: 'attacker123' } }; const victimConversationId = uuidv4(); const victimMessageId = 'victim-msg-123'; // Save a message as the victim - const victimReq = { user: { id: 'victim123' } }; - await saveMessage(victimReq, { - messageId: victimMessageId, - conversationId: victimConversationId, - text: 'Victim message', - user: 'victim123', - }); + await saveMessage( + { userId: 'victim123' }, + { + messageId: victimMessageId, + conversationId: victimConversationId, + text: 'Victim message', + user: 'victim123', + }, + ); // Attacker tries to delete from victim's conversation - const result = await deleteMessagesSince(attackerReq, { + const result = await deleteMessagesSince('attacker123', { messageId: victimMessageId, conversationId: victimConversationId, }); @@ -280,41 +306,45 @@ describe('Message Operations', () => { user: 'victim123', }); expect(victimMessage).toBeTruthy(); - expect(victimMessage.text).toBe('Victim message'); + expect(victimMessage?.text).toBe('Victim message'); }); it("should not allow inserting a new message into another user's conversation", async () => { - const attackerReq = { user: { id: 'attacker123' } }; const victimConversationId = uuidv4(); // Attacker tries to save a message - this should succeed but with attacker's user ID - const result = await saveMessage(attackerReq, { - conversationId: victimConversationId, - text: 'Inserted malicious message', - messageId: 'new-msg-123', - user: 'attacker123', - }); + const result = await saveMessage( + { userId: 'attacker123' }, + { + conversationId: victimConversationId, + text: 'Inserted malicious message', + messageId: 'new-msg-123', + user: 'attacker123', + }, + ); expect(result).toBeTruthy(); - expect(result.user).toBe('attacker123'); + expect(result?.user).toBe('attacker123'); // Verify the message was saved with the attacker's user ID, not as an anonymous message const savedMessage = await Message.findOne({ messageId: 'new-msg-123' }); - expect(savedMessage.user).toBe('attacker123'); - expect(savedMessage.conversationId).toBe(victimConversationId); + expect(savedMessage?.user).toBe('attacker123'); + expect(savedMessage?.conversationId).toBe(victimConversationId); }); it('should allow retrieving messages from any conversation', async () => { const victimConversationId = uuidv4(); // Save a message in the victim's conversation - const victimReq = { user: { id: 'victim123' } }; - await saveMessage(victimReq, { - messageId: 'victim-msg', - conversationId: victimConversationId, - text: 'Victim message', - user: 'victim123', - }); + await saveMessage( + { userId: 'victim123' }, + { + messageId: 'victim-msg', + conversationId: victimConversationId, + text: 'Victim message', + user: 'victim123', + }, + ); // Anyone should be able to retrieve messages by conversation ID const messages = await getMessages({ conversationId: victimConversationId }); @@ -331,21 +361,21 @@ describe('Message Operations', () => { it('should save a message with expiredAt when isTemporary is true', async () => { // Mock app config with 24 hour retention - mockReq.config.interfaceConfig.temporaryChatRetention = 24; + mockCtx.interfaceConfig = { temporaryChatRetention: 24 }; - mockReq.body = { isTemporary: true }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); const afterSave = new Date(); - expect(result.messageId).toBe('msg123'); - expect(result.expiredAt).toBeDefined(); - expect(result.expiredAt).toBeInstanceOf(Date); + expect(result?.messageId).toBe('msg123'); + expect(result?.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeInstanceOf(Date); // Verify expiredAt is approximately 24 hours in the future const expectedExpirationTime = new Date(beforeSave.getTime() + 24 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -356,38 +386,37 @@ describe('Message Operations', () => { }); it('should save a message without expiredAt when isTemporary is false', async () => { - mockReq.body = { isTemporary: false }; + mockCtx.isTemporary = false; - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); - expect(result.messageId).toBe('msg123'); - expect(result.expiredAt).toBeNull(); + expect(result?.messageId).toBe('msg123'); + expect(result?.expiredAt).toBeNull(); }); it('should save a message without expiredAt when isTemporary is not provided', async () => { - // No isTemporary in body - mockReq.body = {}; + // No isTemporary set - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); - expect(result.messageId).toBe('msg123'); - expect(result.expiredAt).toBeNull(); + expect(result?.messageId).toBe('msg123'); + expect(result?.expiredAt).toBeNull(); }); it('should use custom retention period from config', async () => { // Mock app config with 48 hour retention - mockReq.config.interfaceConfig.temporaryChatRetention = 48; + mockCtx.interfaceConfig = { temporaryChatRetention: 48 }; - mockReq.body = { isTemporary: true }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Verify expiredAt is approximately 48 hours in the future const expectedExpirationTime = new Date(beforeSave.getTime() + 48 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -399,18 +428,18 @@ describe('Message Operations', () => { it('should handle minimum retention period (1 hour)', async () => { // Mock app config with less than minimum retention - mockReq.config.interfaceConfig.temporaryChatRetention = 0.5; // Half hour - should be clamped to 1 hour + mockCtx.interfaceConfig = { temporaryChatRetention: 0.5 }; // Half hour - should be clamped to 1 hour - mockReq.body = { isTemporary: true }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Verify expiredAt is approximately 1 hour in the future (minimum) const expectedExpirationTime = new Date(beforeSave.getTime() + 1 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -422,18 +451,18 @@ describe('Message Operations', () => { it('should handle maximum retention period (8760 hours)', async () => { // Mock app config with more than maximum retention - mockReq.config.interfaceConfig.temporaryChatRetention = 10000; // Should be clamped to 8760 hours + mockCtx.interfaceConfig = { temporaryChatRetention: 10000 }; // Should be clamped to 8760 hours - mockReq.body = { isTemporary: true }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Verify expiredAt is approximately 8760 hours (1 year) in the future const expectedExpirationTime = new Date(beforeSave.getTime() + 8760 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -445,22 +474,22 @@ describe('Message Operations', () => { it('should handle missing config gracefully', async () => { // Simulate missing config - should use default retention period - delete mockReq.config; + delete mockCtx.interfaceConfig; - mockReq.body = { isTemporary: true }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); const afterSave = new Date(); // Should still save the message with default retention period (30 days) - expect(result.messageId).toBe('msg123'); - expect(result.expiredAt).toBeDefined(); - expect(result.expiredAt).toBeInstanceOf(Date); + expect(result?.messageId).toBe('msg123'); + expect(result?.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeInstanceOf(Date); // Verify expiredAt is approximately 30 days in the future (720 hours) const expectedExpirationTime = new Date(beforeSave.getTime() + 720 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -472,18 +501,18 @@ describe('Message Operations', () => { it('should use default retention when config is not provided', async () => { // Mock getAppConfig to return empty config - mockReq.config = {}; // Empty config + mockCtx.interfaceConfig = undefined; // Empty config - mockReq.body = { isTemporary: true }; + mockCtx.isTemporary = true; const beforeSave = new Date(); - const result = await saveMessage(mockReq, mockMessageData); + const result = await saveMessage(mockCtx, mockMessageData); - expect(result.expiredAt).toBeDefined(); + expect(result?.expiredAt).toBeDefined(); // Default retention is 30 days (720 hours) const expectedExpirationTime = new Date(beforeSave.getTime() + 30 * 24 * 60 * 60 * 1000); - const actualExpirationTime = new Date(result.expiredAt); + const actualExpirationTime = new Date(result?.expiredAt ?? 0); expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( expectedExpirationTime.getTime() - 1000, @@ -495,47 +524,47 @@ describe('Message Operations', () => { it('should not update expiredAt on message update', async () => { // First save a temporary message - mockReq.config.interfaceConfig.temporaryChatRetention = 24; + mockCtx.interfaceConfig = { temporaryChatRetention: 24 }; - mockReq.body = { isTemporary: true }; - const savedMessage = await saveMessage(mockReq, mockMessageData); - const originalExpiredAt = savedMessage.expiredAt; + mockCtx.isTemporary = true; + const savedMessage = await saveMessage(mockCtx, mockMessageData); + const originalExpiredAt = savedMessage?.expiredAt; // Now update the message without isTemporary flag - mockReq.body = {}; - const updatedMessage = await updateMessage(mockReq, { + mockCtx.isTemporary = undefined; + const updatedMessage = await updateMessage(mockCtx.userId, { messageId: 'msg123', text: 'Updated text', }); // expiredAt should not be in the returned updated message object - expect(updatedMessage.expiredAt).toBeUndefined(); + expect(updatedMessage?.expiredAt).toBeUndefined(); // Verify in database that expiredAt wasn't changed const dbMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); - expect(dbMessage.expiredAt).toEqual(originalExpiredAt); + expect(dbMessage?.expiredAt).toEqual(originalExpiredAt); }); it('should preserve expiredAt when saving existing temporary message', async () => { // First save a temporary message - mockReq.config.interfaceConfig.temporaryChatRetention = 24; + mockCtx.interfaceConfig = { temporaryChatRetention: 24 }; - mockReq.body = { isTemporary: true }; - const firstSave = await saveMessage(mockReq, mockMessageData); - const originalExpiredAt = firstSave.expiredAt; + mockCtx.isTemporary = true; + const firstSave = await saveMessage(mockCtx, mockMessageData); + const originalExpiredAt = firstSave?.expiredAt; // Wait a bit to ensure time difference await new Promise((resolve) => setTimeout(resolve, 100)); // Save again with same messageId but different text const updatedData = { ...mockMessageData, text: 'Updated text' }; - const secondSave = await saveMessage(mockReq, updatedData); + const secondSave = await saveMessage(mockCtx, updatedData); // Should update text but create new expiredAt - expect(secondSave.text).toBe('Updated text'); - expect(secondSave.expiredAt).toBeDefined(); - expect(new Date(secondSave.expiredAt).getTime()).toBeGreaterThan( - new Date(originalExpiredAt).getTime(), + expect(secondSave?.text).toBe('Updated text'); + expect(secondSave?.expiredAt).toBeDefined(); + expect(new Date(secondSave?.expiredAt ?? 0).getTime()).toBeGreaterThan( + new Date(originalExpiredAt ?? 0).getTime(), ); }); @@ -569,8 +598,8 @@ describe('Message Operations', () => { const bulk1 = savedMessages.find((m) => m.messageId === 'bulk1'); const bulk2 = savedMessages.find((m) => m.messageId === 'bulk2'); - expect(bulk1.expiredAt).toBeDefined(); - expect(bulk2.expiredAt).toBeNull(); + expect(bulk1?.expiredAt).toBeDefined(); + expect(bulk2?.expiredAt).toBeNull(); }); }); @@ -579,7 +608,11 @@ describe('Message Operations', () => { * Helper to create messages with specific timestamps * Uses collection.insertOne to bypass Mongoose timestamps */ - const createMessageWithTimestamp = async (index, conversationId, createdAt) => { + const createMessageWithTimestamp = async ( + index: number, + conversationId: string, + createdAt: Date, + ) => { const messageId = uuidv4(); await Message.collection.insertOne({ messageId, @@ -601,15 +634,22 @@ describe('Message Operations', () => { conversationId, user, pageSize = 25, - cursor = null, + cursor = null as string | null, sortBy = 'createdAt', sortDirection = 'desc', + }: { + conversationId: string; + user: string; + pageSize?: number; + cursor?: string | null; + sortBy?: string; + sortDirection?: string; }) => { const sortOrder = sortDirection === 'asc' ? 1 : -1; const sortField = ['createdAt', 'updatedAt'].includes(sortBy) ? sortBy : 'createdAt'; const cursorOperator = sortDirection === 'asc' ? '$gt' : '$lt'; - const filter = { conversationId, user }; + const filter: Record = { conversationId, user }; if (cursor) { filter[sortField] = { [cursorOperator]: new Date(cursor) }; } @@ -619,11 +659,13 @@ describe('Message Operations', () => { .limit(pageSize + 1) .lean(); - let nextCursor = null; + let nextCursor: string | null = null; if (messages.length > pageSize) { messages.pop(); // Remove extra item used to detect next page // Create cursor from the last RETURNED item (not the popped one) - nextCursor = messages[messages.length - 1][sortField]; + nextCursor = (messages[messages.length - 1] as Record)[ + sortField + ] as string; } return { messages, nextCursor }; @@ -677,7 +719,7 @@ describe('Message Operations', () => { const baseTime = new Date('2026-01-01T12:00:00.000Z'); // Create exactly 26 messages - const messages = []; + const messages: (IMessage | null)[] = []; for (let i = 0; i < 26; i++) { const createdAt = new Date(baseTime.getTime() - i * 60000); const msg = await createMessageWithTimestamp(i, conversationId, createdAt); @@ -699,7 +741,7 @@ describe('Message Operations', () => { // Item 26 should NOT be in page 1 const page1Ids = page1.messages.map((m) => m.messageId); - expect(page1Ids).not.toContain(item26.messageId); + expect(page1Ids).not.toContain(item26!.messageId); // Fetch second page const page2 = await getMessagesByCursor({ @@ -711,7 +753,7 @@ describe('Message Operations', () => { // Item 26 MUST be in page 2 (this was the bug - it was being skipped) expect(page2.messages).toHaveLength(1); - expect(page2.messages[0].messageId).toBe(item26.messageId); + expect((page2.messages[0] as { messageId: string }).messageId).toBe(item26!.messageId); }); it('should sort by createdAt DESC by default', async () => { @@ -740,10 +782,10 @@ describe('Message Operations', () => { }); // Should be sorted by createdAt DESC (newest first) by default - expect(result.messages).toHaveLength(3); - expect(result.messages[0].messageId).toBe(msg3.messageId); - expect(result.messages[1].messageId).toBe(msg2.messageId); - expect(result.messages[2].messageId).toBe(msg1.messageId); + expect(result?.messages).toHaveLength(3); + expect((result?.messages[0] as { messageId: string }).messageId).toBe(msg3!.messageId); + expect((result?.messages[1] as { messageId: string }).messageId).toBe(msg2!.messageId); + expect((result?.messages[2] as { messageId: string }).messageId).toBe(msg1!.messageId); }); it('should support ascending sort direction', async () => { @@ -767,9 +809,9 @@ describe('Message Operations', () => { }); // Should be sorted by createdAt ASC (oldest first) - expect(result.messages).toHaveLength(2); - expect(result.messages[0].messageId).toBe(msg1.messageId); - expect(result.messages[1].messageId).toBe(msg2.messageId); + expect(result?.messages).toHaveLength(2); + expect((result?.messages[0] as { messageId: string }).messageId).toBe(msg1!.messageId); + expect((result?.messages[1] as { messageId: string }).messageId).toBe(msg2!.messageId); }); it('should handle empty conversation', async () => { @@ -780,8 +822,8 @@ describe('Message Operations', () => { user: 'user123', }); - expect(result.messages).toHaveLength(0); - expect(result.nextCursor).toBeNull(); + expect(result?.messages).toHaveLength(0); + expect(result?.nextCursor).toBeNull(); }); it('should only return messages for the specified user', async () => { @@ -814,8 +856,8 @@ describe('Message Operations', () => { }); // Should only return user123's message - expect(result.messages).toHaveLength(1); - expect(result.messages[0].user).toBe('user123'); + expect(result?.messages).toHaveLength(1); + expect((result?.messages[0] as { user: string }).user).toBe('user123'); }); it('should handle exactly pageSize number of messages (no next page)', async () => { @@ -834,8 +876,8 @@ describe('Message Operations', () => { pageSize: 25, }); - expect(result.messages).toHaveLength(25); - expect(result.nextCursor).toBeNull(); // No next page + expect(result?.messages).toHaveLength(25); + expect(result?.nextCursor).toBeNull(); // No next page }); it('should handle pageSize of 1', async () => { @@ -849,8 +891,8 @@ describe('Message Operations', () => { } // Fetch with pageSize 1 - let cursor = null; - const allMessages = []; + let cursor: string | null = null; + const allMessages: unknown[] = []; for (let page = 0; page < 5; page++) { const result = await getMessagesByCursor({ @@ -860,8 +902,8 @@ describe('Message Operations', () => { cursor, }); - allMessages.push(...result.messages); - cursor = result.nextCursor; + allMessages.push(...(result?.messages ?? [])); + cursor = result?.nextCursor; if (!cursor) { break; @@ -870,7 +912,7 @@ describe('Message Operations', () => { // Should get all 3 messages without duplicates expect(allMessages).toHaveLength(3); - const uniqueIds = new Set(allMessages.map((m) => m.messageId)); + const uniqueIds = new Set(allMessages.map((m) => (m as { messageId: string }).messageId)); expect(uniqueIds.size).toBe(3); }); @@ -879,7 +921,7 @@ describe('Message Operations', () => { const sameTime = new Date('2026-01-01T12:00:00.000Z'); // Create multiple messages with the exact same timestamp - const messages = []; + const messages: (IMessage | null)[] = []; for (let i = 0; i < 5; i++) { const msg = await createMessageWithTimestamp(i, conversationId, sameTime); messages.push(msg); @@ -892,7 +934,7 @@ describe('Message Operations', () => { }); // All messages should be returned - expect(result.messages).toHaveLength(5); + expect(result?.messages).toHaveLength(5); }); }); }); diff --git a/packages/data-schemas/src/methods/message.ts b/packages/data-schemas/src/methods/message.ts new file mode 100644 index 0000000000..ae5ca72b12 --- /dev/null +++ b/packages/data-schemas/src/methods/message.ts @@ -0,0 +1,399 @@ +import type { DeleteResult, FilterQuery, Model } from 'mongoose'; +import logger from '~/config/winston'; +import { createTempChatExpirationDate } from '~/utils/tempChatRetention'; +import type { AppConfig, IMessage } from '~/types'; + +/** Simple UUID v4 regex to replace zod validation */ +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export interface MessageMethods { + saveMessage( + ctx: { userId: string; isTemporary?: boolean; interfaceConfig?: AppConfig['interfaceConfig'] }, + params: Partial & { newMessageId?: string }, + metadata?: { context?: string }, + ): Promise; + bulkSaveMessages( + messages: Array>, + overrideTimestamp?: boolean, + ): Promise; + recordMessage(params: { + user: string; + endpoint?: string; + messageId: string; + conversationId?: string; + parentMessageId?: string; + [key: string]: unknown; + }): Promise; + updateMessageText(userId: string, params: { messageId: string; text: string }): Promise; + updateMessage( + userId: string, + message: Partial & { newMessageId?: string }, + metadata?: { context?: string }, + ): Promise>; + deleteMessagesSince( + userId: string, + params: { messageId: string; conversationId: string }, + ): Promise; + getMessages(filter: FilterQuery, select?: string): Promise; + getMessage(params: { user: string; messageId: string }): Promise; + getMessagesByCursor( + filter: FilterQuery, + options?: { + sortField?: string; + sortOrder?: 1 | -1; + limit?: number; + cursor?: string | null; + }, + ): Promise<{ messages: IMessage[]; nextCursor: string | null }>; + searchMessages( + query: string, + searchOptions: Partial, + hydrate?: boolean, + ): Promise; + deleteMessages(filter: FilterQuery): Promise; +} + +export function createMessageMethods(mongoose: typeof import('mongoose')): MessageMethods { + /** + * Saves a message in the database. + */ + async function saveMessage( + { + userId, + isTemporary, + interfaceConfig, + }: { + userId: string; + isTemporary?: boolean; + interfaceConfig?: AppConfig['interfaceConfig']; + }, + params: Partial & { newMessageId?: string }, + metadata?: { context?: string }, + ) { + if (!userId) { + throw new Error('User not authenticated'); + } + + const conversationId = params.conversationId as string | undefined; + if (!conversationId || !UUID_REGEX.test(conversationId)) { + logger.warn(`Invalid conversation ID: ${conversationId}`); + logger.info(`---\`saveMessage\` context: ${metadata?.context}`); + logger.info(`---Invalid conversation ID Params: ${JSON.stringify(params, null, 2)}`); + return; + } + + try { + const Message = mongoose.models.Message as Model; + const update: Record = { + ...params, + user: userId, + messageId: params.newMessageId || params.messageId, + }; + + if (isTemporary) { + try { + update.expiredAt = createTempChatExpirationDate(interfaceConfig); + } catch (err) { + logger.error('Error creating temporary chat expiration date:', err); + logger.info(`---\`saveMessage\` context: ${metadata?.context}`); + update.expiredAt = null; + } + } else { + update.expiredAt = null; + } + + if (update.tokenCount != null && isNaN(update.tokenCount as number)) { + logger.warn( + `Resetting invalid \`tokenCount\` for message \`${params.messageId}\`: ${update.tokenCount}`, + ); + logger.info(`---\`saveMessage\` context: ${metadata?.context}`); + update.tokenCount = 0; + } + const message = await Message.findOneAndUpdate( + { messageId: params.messageId, user: userId }, + update, + { upsert: true, new: true }, + ); + + return message.toObject(); + } catch (err: unknown) { + logger.error('Error saving message:', err); + logger.info(`---\`saveMessage\` context: ${metadata?.context}`); + + const mongoErr = err as { code?: number; message?: string }; + if (mongoErr.code === 11000 && mongoErr.message?.includes('duplicate key error')) { + logger.warn(`Duplicate messageId detected: ${params.messageId}. Continuing execution.`); + + try { + const Message = mongoose.models.Message as Model; + const existingMessage = await Message.findOne({ + messageId: params.messageId, + user: userId, + }); + + if (existingMessage) { + return existingMessage.toObject(); + } + + return undefined; + } catch (findError) { + logger.warn( + `Could not retrieve existing message with ID ${params.messageId}: ${(findError as Error).message}`, + ); + return undefined; + } + } + + throw err; + } + } + + /** + * Saves multiple messages in bulk. + */ + async function bulkSaveMessages( + messages: Array>, + overrideTimestamp = false, + ) { + try { + const Message = mongoose.models.Message as Model; + const bulkOps = messages.map((message) => ({ + updateOne: { + filter: { messageId: message.messageId }, + update: message, + timestamps: !overrideTimestamp, + upsert: true, + }, + })); + const result = await Message.bulkWrite(bulkOps); + return result; + } catch (err) { + logger.error('Error saving messages in bulk:', err); + throw err; + } + } + + /** + * Records a message in the database (no UUID validation). + */ + async function recordMessage({ + user, + endpoint, + messageId, + conversationId, + parentMessageId, + ...rest + }: { + user: string; + endpoint?: string; + messageId: string; + conversationId?: string; + parentMessageId?: string; + [key: string]: unknown; + }) { + try { + const Message = mongoose.models.Message as Model; + const message = { + user, + endpoint, + messageId, + conversationId, + parentMessageId, + ...rest, + }; + + return await Message.findOneAndUpdate({ user, messageId }, message, { + upsert: true, + new: true, + }); + } catch (err) { + logger.error('Error recording message:', err); + throw err; + } + } + + /** + * Updates the text of a message. + */ + async function updateMessageText( + userId: string, + { messageId, text }: { messageId: string; text: string }, + ) { + try { + const Message = mongoose.models.Message as Model; + await Message.updateOne({ messageId, user: userId }, { text }); + } catch (err) { + logger.error('Error updating message text:', err); + throw err; + } + } + + /** + * Updates a message and returns sanitized fields. + */ + async function updateMessage( + userId: string, + message: { messageId: string; [key: string]: unknown }, + metadata?: { context?: string }, + ) { + try { + const Message = mongoose.models.Message as Model; + const { messageId, ...update } = message; + const updatedMessage = await Message.findOneAndUpdate({ messageId, user: userId }, update, { + new: true, + }); + + if (!updatedMessage) { + throw new Error('Message not found or user not authorized.'); + } + + return { + messageId: updatedMessage.messageId, + conversationId: updatedMessage.conversationId, + parentMessageId: updatedMessage.parentMessageId, + sender: updatedMessage.sender, + text: updatedMessage.text, + isCreatedByUser: updatedMessage.isCreatedByUser, + tokenCount: updatedMessage.tokenCount, + feedback: updatedMessage.feedback, + }; + } catch (err) { + logger.error('Error updating message:', err); + if (metadata?.context) { + logger.info(`---\`updateMessage\` context: ${metadata.context}`); + } + throw err; + } + } + + /** + * Deletes messages in a conversation since a specific message. + */ + async function deleteMessagesSince( + userId: string, + { messageId, conversationId }: { messageId: string; conversationId: string }, + ) { + try { + const Message = mongoose.models.Message as Model; + const message = await Message.findOne({ messageId, user: userId }).lean(); + + if (message) { + const query = Message.find({ conversationId, user: userId }); + return await query.deleteMany({ + createdAt: { $gt: message.createdAt }, + }); + } + return undefined; + } catch (err) { + logger.error('Error deleting messages:', err); + throw err; + } + } + + /** + * Retrieves messages from the database. + */ + async function getMessages(filter: FilterQuery, select?: string) { + try { + const Message = mongoose.models.Message as Model; + if (select) { + return await Message.find(filter).select(select).sort({ createdAt: 1 }).lean(); + } + + return await Message.find(filter).sort({ createdAt: 1 }).lean(); + } catch (err) { + logger.error('Error getting messages:', err); + throw err; + } + } + + /** + * Retrieves a single message from the database. + */ + async function getMessage({ user, messageId }: { user: string; messageId: string }) { + try { + const Message = mongoose.models.Message as Model; + return await Message.findOne({ user, messageId }).lean(); + } catch (err) { + logger.error('Error getting message:', err); + throw err; + } + } + + /** + * Deletes messages from the database. + */ + async function deleteMessages(filter: FilterQuery) { + try { + const Message = mongoose.models.Message as Model; + return await Message.deleteMany(filter); + } catch (err) { + logger.error('Error deleting messages:', err); + throw err; + } + } + + /** + * Retrieves paginated messages with custom sorting and cursor support. + */ + async function getMessagesByCursor( + filter: FilterQuery, + options: { + sortField?: string; + sortOrder?: 1 | -1; + limit?: number; + cursor?: string | null; + } = {}, + ) { + const Message = mongoose.models.Message as Model; + const { sortField = 'createdAt', sortOrder = -1, limit = 25, cursor } = options; + const queryFilter = { ...filter }; + if (cursor) { + queryFilter[sortField] = sortOrder === 1 ? { $gt: cursor } : { $lt: cursor }; + } + const messages = await Message.find(queryFilter) + .sort({ [sortField]: sortOrder }) + .limit(limit + 1) + .lean(); + + let nextCursor: string | null = null; + if (messages.length > limit) { + messages.pop(); + const last = messages[messages.length - 1] as Record; + nextCursor = String(last[sortField] ?? ''); + } + return { messages, nextCursor }; + } + + /** + * Performs a MeiliSearch query on the Message collection. + * Requires the meilisearch plugin to be registered on the Message model. + */ + async function searchMessages( + query: string, + searchOptions: Record, + hydrate?: boolean, + ) { + const Message = mongoose.models.Message as Model & { + meiliSearch?: (q: string, opts: Record, h?: boolean) => Promise; + }; + if (typeof Message.meiliSearch !== 'function') { + throw new Error('MeiliSearch plugin not registered on Message model'); + } + return Message.meiliSearch(query, searchOptions, hydrate); + } + + return { + saveMessage, + bulkSaveMessages, + recordMessage, + updateMessageText, + updateMessage, + deleteMessagesSince, + getMessages, + getMessage, + getMessagesByCursor, + searchMessages, + deleteMessages, + }; +} diff --git a/packages/data-schemas/src/methods/preset.ts b/packages/data-schemas/src/methods/preset.ts new file mode 100644 index 0000000000..11af817cbd --- /dev/null +++ b/packages/data-schemas/src/methods/preset.ts @@ -0,0 +1,132 @@ +import type { Model } from 'mongoose'; +import logger from '~/config/winston'; + +interface IPreset { + user?: string; + presetId?: string; + order?: number; + defaultPreset?: boolean; + tools?: (string | { pluginKey?: string })[]; + updatedAt?: Date; + [key: string]: unknown; +} + +export function createPresetMethods(mongoose: typeof import('mongoose')) { + /** + * Retrieves a single preset by user and presetId. + */ + async function getPreset(user: string, presetId: string) { + try { + const Preset = mongoose.models.Preset as Model; + return await Preset.findOne({ user, presetId }).lean(); + } catch (error) { + logger.error('[getPreset] Error getting single preset', error); + return { message: 'Error getting single preset' }; + } + } + + /** + * Retrieves all presets for a user, sorted by order then updatedAt. + */ + async function getPresets(user: string, filter: Record = {}) { + try { + const Preset = mongoose.models.Preset as Model; + const presets = await Preset.find({ ...filter, user }).lean(); + const defaultValue = 10000; + + presets.sort((a, b) => { + const orderA = a.order !== undefined ? a.order : defaultValue; + const orderB = b.order !== undefined ? b.order : defaultValue; + + if (orderA !== orderB) { + return orderA - orderB; + } + + return new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime(); + }); + + return presets; + } catch (error) { + logger.error('[getPresets] Error getting presets', error); + return { message: 'Error retrieving presets' }; + } + } + + /** + * Saves a preset. Handles default preset logic and tool normalization. + */ + async function savePreset( + user: string, + { + presetId, + newPresetId, + defaultPreset, + ...preset + }: { + presetId?: string; + newPresetId?: string; + defaultPreset?: boolean; + [key: string]: unknown; + }, + ) { + try { + const Preset = mongoose.models.Preset as Model; + const setter: Record = { $set: {} }; + const { user: _unusedUser, ...cleanPreset } = preset; + const update: Record = { presetId, ...cleanPreset }; + if (preset.tools && Array.isArray(preset.tools)) { + update.tools = + (preset.tools as Array) + .map((tool) => (typeof tool === 'object' && tool?.pluginKey ? tool.pluginKey : tool)) + .filter((toolName) => typeof toolName === 'string') ?? []; + } + if (newPresetId) { + update.presetId = newPresetId; + } + + if (defaultPreset) { + update.defaultPreset = defaultPreset; + update.order = 0; + + const currentDefault = await Preset.findOne({ defaultPreset: true, user }); + + if (currentDefault && currentDefault.presetId !== presetId) { + await Preset.findByIdAndUpdate(currentDefault._id, { + $unset: { defaultPreset: '', order: '' }, + }); + } + } else if (defaultPreset === false) { + update.defaultPreset = undefined; + update.order = undefined; + setter['$unset'] = { defaultPreset: '', order: '' }; + } + + setter.$set = update; + return await Preset.findOneAndUpdate({ presetId, user }, setter, { + new: true, + upsert: true, + }); + } catch (error) { + logger.error('[savePreset] Error saving preset', error); + return { message: 'Error saving preset' }; + } + } + + /** + * Deletes presets matching the given filter for a user. + */ + async function deletePresets(user: string, filter: Record = {}) { + const Preset = mongoose.models.Preset as Model; + const deleteCount = await Preset.deleteMany({ ...filter, user }); + return deleteCount; + } + + return { + getPreset, + getPresets, + savePreset, + deletePresets, + }; +} + +export type PresetMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/prompt.spec.ts b/packages/data-schemas/src/methods/prompt.spec.ts new file mode 100644 index 0000000000..0a8c2c247e --- /dev/null +++ b/packages/data-schemas/src/methods/prompt.spec.ts @@ -0,0 +1,627 @@ +import mongoose from 'mongoose'; +import { ObjectId } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + SystemRoles, + ResourceType, + AccessRoleIds, + PrincipalType, + PermissionBits, +} from 'librechat-data-provider'; +import type { IPromptGroup, AccessRole as TAccessRole, AclEntry as TAclEntry } from '..'; +import { createAclEntryMethods } from './aclEntry'; +import { logger, createModels } from '..'; +import { createMethods } from './index'; + +// Disable console for tests +logger.silent = true; + +/** Lean user object from .toObject() */ +type LeanUser = { + _id: mongoose.Types.ObjectId | string; + name?: string; + email: string; + role?: string; +}; + +/** Lean group object from .toObject() */ +type LeanGroup = { + _id: mongoose.Types.ObjectId | string; + name: string; + description?: string; +}; + +/** Lean access role object from .toObject() / .lean() */ +type LeanAccessRole = TAccessRole & { _id: mongoose.Types.ObjectId | string }; + +/** Lean ACL entry from .lean() */ +type LeanAclEntry = TAclEntry & { _id: mongoose.Types.ObjectId | string }; + +/** Lean prompt group from .toObject() */ +type LeanPromptGroup = IPromptGroup & { _id: mongoose.Types.ObjectId | string }; + +let Prompt: mongoose.Model; +let PromptGroup: mongoose.Model; +let AclEntry: mongoose.Model; +let AccessRole: mongoose.Model; +let User: mongoose.Model; +let Group: mongoose.Model; +let methods: ReturnType; +let aclMethods: ReturnType; +let testUsers: Record; +let testGroups: Record; +let testRoles: Record; + +let mongoServer: MongoMemoryServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + + createModels(mongoose); + Prompt = mongoose.models.Prompt; + PromptGroup = mongoose.models.PromptGroup; + AclEntry = mongoose.models.AclEntry; + AccessRole = mongoose.models.AccessRole; + User = mongoose.models.User; + Group = mongoose.models.Group; + + methods = createMethods(mongoose, { + removeAllPermissions: async ({ resourceType, resourceId }) => { + await AclEntry.deleteMany({ resourceType, resourceId }); + }, + }); + aclMethods = createAclEntryMethods(mongoose); + + await setupTestData(); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +async function setupTestData() { + testRoles = { + viewer: ( + await AccessRole.create({ + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, + name: 'Viewer', + description: 'Can view promptGroups', + resourceType: ResourceType.PROMPTGROUP, + permBits: PermissionBits.VIEW, + }) + ).toObject() as unknown as LeanAccessRole, + editor: ( + await AccessRole.create({ + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, + name: 'Editor', + description: 'Can view and edit promptGroups', + resourceType: ResourceType.PROMPTGROUP, + permBits: PermissionBits.VIEW | PermissionBits.EDIT, + }) + ).toObject() as unknown as LeanAccessRole, + owner: ( + await AccessRole.create({ + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + name: 'Owner', + description: 'Full control over promptGroups', + resourceType: ResourceType.PROMPTGROUP, + permBits: + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, + }) + ).toObject() as unknown as LeanAccessRole, + }; + + testUsers = { + owner: ( + await User.create({ + name: 'Prompt Owner', + email: 'owner@example.com', + role: SystemRoles.USER, + }) + ).toObject() as unknown as LeanUser, + editor: ( + await User.create({ + name: 'Prompt Editor', + email: 'editor@example.com', + role: SystemRoles.USER, + }) + ).toObject() as unknown as LeanUser, + viewer: ( + await User.create({ + name: 'Prompt Viewer', + email: 'viewer@example.com', + role: SystemRoles.USER, + }) + ).toObject() as unknown as LeanUser, + admin: ( + await User.create({ + name: 'Admin User', + email: 'admin@example.com', + role: SystemRoles.ADMIN, + }) + ).toObject() as unknown as LeanUser, + noAccess: ( + await User.create({ + name: 'No Access User', + email: 'noaccess@example.com', + role: SystemRoles.USER, + }) + ).toObject() as unknown as LeanUser, + }; + + testGroups = { + editors: ( + await Group.create({ + name: 'Prompt Editors', + description: 'Group with editor access', + }) + ).toObject() as unknown as LeanGroup, + viewers: ( + await Group.create({ + name: 'Prompt Viewers', + description: 'Group with viewer access', + }) + ).toObject() as unknown as LeanGroup, + }; +} + +/** Helper: grant permission via direct AclEntry.create */ +async function grantPermission(params: { + principalType: string; + principalId: mongoose.Types.ObjectId | string; + resourceType: string; + resourceId: mongoose.Types.ObjectId | string; + accessRoleId: string; + grantedBy: mongoose.Types.ObjectId | string; +}) { + const role = (await AccessRole.findOne({ + accessRoleId: params.accessRoleId, + }).lean()) as LeanAccessRole | null; + if (!role) { + throw new Error(`AccessRole ${params.accessRoleId} not found`); + } + return aclMethods.grantPermission( + params.principalType, + params.principalId, + params.resourceType, + params.resourceId, + role.permBits, + params.grantedBy, + undefined, + role._id, + ); +} + +/** Helper: check permission via getUserPrincipals + hasPermission */ +async function checkPermission(params: { + userId: mongoose.Types.ObjectId | string; + resourceType: string; + resourceId: mongoose.Types.ObjectId | string; + requiredPermission: number; + includePublic?: boolean; +}) { + // getUserPrincipals already includes user, role, groups, and public + const principals = await methods.getUserPrincipals({ + userId: params.userId, + }); + + // If not including public, filter it out + const filteredPrincipals = params.includePublic + ? principals + : principals.filter((p) => p.principalType !== PrincipalType.PUBLIC); + + return aclMethods.hasPermission( + filteredPrincipals, + params.resourceType, + params.resourceId, + params.requiredPermission, + ); +} + +describe('Prompt ACL Permissions', () => { + describe('Creating Prompts with Permissions', () => { + it('should grant owner permissions when creating a prompt', async () => { + const testGroup = ( + await PromptGroup.create({ + name: 'Test Group', + category: 'testing', + author: testUsers.owner._id, + authorName: testUsers.owner.name, + productionId: new mongoose.Types.ObjectId(), + }) + ).toObject() as unknown as LeanPromptGroup; + + const promptData = { + prompt: { + prompt: 'Test prompt content', + name: 'Test Prompt', + type: 'text', + groupId: testGroup._id, + }, + author: testUsers.owner._id, + }; + + await methods.savePrompt(promptData); + + // Grant owner permission + await grantPermission({ + principalType: PrincipalType.USER, + principalId: testUsers.owner._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: testUsers.owner._id, + }); + + // Check ACL entry + const aclEntry = (await AclEntry.findOne({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: testGroup._id, + principalType: PrincipalType.USER, + principalId: testUsers.owner._id, + }).lean()) as LeanAclEntry | null; + + expect(aclEntry).toBeTruthy(); + expect(aclEntry!.permBits).toBe(testRoles.owner.permBits); + }); + }); + + describe('Accessing Prompts', () => { + let testPromptGroup: LeanPromptGroup; + + beforeEach(async () => { + testPromptGroup = ( + await PromptGroup.create({ + name: 'Test Group', + author: testUsers.owner._id, + authorName: testUsers.owner.name, + productionId: new ObjectId(), + }) + ).toObject() as unknown as LeanPromptGroup; + + await Prompt.create({ + prompt: 'Test prompt for access control', + name: 'Access Test Prompt', + author: testUsers.owner._id, + groupId: testPromptGroup._id, + type: 'text', + }); + + await grantPermission({ + principalType: PrincipalType.USER, + principalId: testUsers.owner._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: testUsers.owner._id, + }); + }); + + afterEach(async () => { + await Prompt.deleteMany({}); + await PromptGroup.deleteMany({}); + await AclEntry.deleteMany({}); + }); + + it('owner should have full access to their prompt', async () => { + const hasAccess = await checkPermission({ + userId: testUsers.owner._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.VIEW, + }); + + expect(hasAccess).toBe(true); + + const canEdit = await checkPermission({ + userId: testUsers.owner._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.EDIT, + }); + + expect(canEdit).toBe(true); + }); + + it('user with viewer role should only have view access', async () => { + await grantPermission({ + principalType: PrincipalType.USER, + principalId: testUsers.viewer._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, + grantedBy: testUsers.owner._id, + }); + + const canView = await checkPermission({ + userId: testUsers.viewer._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.VIEW, + }); + + const canEdit = await checkPermission({ + userId: testUsers.viewer._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.EDIT, + }); + + expect(canView).toBe(true); + expect(canEdit).toBe(false); + }); + + it('user without permissions should have no access', async () => { + const hasAccess = await checkPermission({ + userId: testUsers.noAccess._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.VIEW, + }); + + expect(hasAccess).toBe(false); + }); + + it('admin should have access regardless of permissions', async () => { + // Admin users should work through normal permission system + // The middleware layer handles admin bypass, not the permission service + const hasAccess = await checkPermission({ + userId: testUsers.admin._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.VIEW, + }); + + // Without explicit permissions, even admin won't have access at this layer + expect(hasAccess).toBe(false); + + // The actual admin bypass happens in the middleware layer + }); + }); + + describe('Group-based Access', () => { + afterEach(async () => { + await Prompt.deleteMany({}); + await AclEntry.deleteMany({}); + await User.updateMany({}, { $set: { groups: [] } }); + }); + + it('group members should inherit group permissions', async () => { + const testPromptGroup = ( + await PromptGroup.create({ + name: 'Group Test Group', + author: testUsers.owner._id, + authorName: testUsers.owner.name, + productionId: new ObjectId(), + }) + ).toObject() as unknown as LeanPromptGroup; + + // Add user to group + await methods.addUserToGroup(testUsers.editor._id, testGroups.editors._id); + + await methods.savePrompt({ + author: testUsers.owner._id, + prompt: { + prompt: 'Group test prompt', + name: 'Group Test', + groupId: testPromptGroup._id, + type: 'text', + }, + }); + + // Grant edit permissions to the group + await grantPermission({ + principalType: PrincipalType.GROUP, + principalId: testGroups.editors._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, + grantedBy: testUsers.owner._id, + }); + + // Check if group member has access + const hasAccess = await checkPermission({ + userId: testUsers.editor._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.EDIT, + }); + + expect(hasAccess).toBe(true); + + // Check that non-member doesn't have access + const nonMemberAccess = await checkPermission({ + userId: testUsers.viewer._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + requiredPermission: PermissionBits.EDIT, + }); + + expect(nonMemberAccess).toBe(false); + }); + }); + + describe('Public Access', () => { + let publicPromptGroup: LeanPromptGroup; + let privatePromptGroup: LeanPromptGroup; + + beforeEach(async () => { + publicPromptGroup = ( + await PromptGroup.create({ + name: 'Public Access Test Group', + author: testUsers.owner._id, + authorName: testUsers.owner.name, + productionId: new ObjectId(), + }) + ).toObject() as unknown as LeanPromptGroup; + + privatePromptGroup = ( + await PromptGroup.create({ + name: 'Private Access Test Group', + author: testUsers.owner._id, + authorName: testUsers.owner.name, + productionId: new ObjectId(), + }) + ).toObject() as unknown as LeanPromptGroup; + + await Prompt.create({ + prompt: 'Public prompt', + name: 'Public', + author: testUsers.owner._id, + groupId: publicPromptGroup._id, + type: 'text', + }); + + await Prompt.create({ + prompt: 'Private prompt', + name: 'Private', + author: testUsers.owner._id, + groupId: privatePromptGroup._id, + type: 'text', + }); + + // Grant public view access + await aclMethods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.PROMPTGROUP, + publicPromptGroup._id, + PermissionBits.VIEW, + testUsers.owner._id, + ); + + // Grant only owner access to private + await grantPermission({ + principalType: PrincipalType.USER, + principalId: testUsers.owner._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: privatePromptGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: testUsers.owner._id, + }); + }); + + afterEach(async () => { + await Prompt.deleteMany({}); + await PromptGroup.deleteMany({}); + await AclEntry.deleteMany({}); + }); + + it('public prompt should be accessible to any user', async () => { + const hasAccess = await checkPermission({ + userId: testUsers.noAccess._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: publicPromptGroup._id, + requiredPermission: PermissionBits.VIEW, + includePublic: true, + }); + + expect(hasAccess).toBe(true); + }); + + it('private prompt should not be accessible to unauthorized users', async () => { + const hasAccess = await checkPermission({ + userId: testUsers.noAccess._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: privatePromptGroup._id, + requiredPermission: PermissionBits.VIEW, + includePublic: true, + }); + + expect(hasAccess).toBe(false); + }); + }); + + describe('Prompt Deletion', () => { + it('should remove ACL entries when prompt is deleted', async () => { + const testPromptGroup = ( + await PromptGroup.create({ + name: 'Deletion Test Group', + author: testUsers.owner._id, + authorName: testUsers.owner.name, + productionId: new ObjectId(), + }) + ).toObject() as unknown as LeanPromptGroup; + + const result = await methods.savePrompt({ + author: testUsers.owner._id, + prompt: { + prompt: 'To be deleted', + name: 'Delete Test', + groupId: testPromptGroup._id, + type: 'text', + }, + }); + + const savedPrompt = result as { prompt?: { _id: mongoose.Types.ObjectId } } | null; + if (!savedPrompt?.prompt) { + throw new Error('Failed to save prompt'); + } + const testPromptId = savedPrompt.prompt._id; + + await grantPermission({ + principalType: PrincipalType.USER, + principalId: testUsers.owner._id, + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, + grantedBy: testUsers.owner._id, + }); + + // Verify ACL entry exists + const beforeDelete = await AclEntry.find({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + }); + expect(beforeDelete).toHaveLength(1); + + // Delete the prompt + await methods.deletePrompt({ + promptId: testPromptId, + groupId: testPromptGroup._id, + author: testUsers.owner._id, + role: SystemRoles.USER, + }); + + // Verify ACL entries are removed + const aclEntries = await AclEntry.find({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: testPromptGroup._id, + }); + + expect(aclEntries).toHaveLength(0); + }); + }); + + describe('Backwards Compatibility', () => { + it('should handle prompts without ACL entries gracefully', async () => { + const promptGroup = ( + await PromptGroup.create({ + name: 'Legacy Test Group', + author: testUsers.owner._id, + authorName: testUsers.owner.name, + productionId: new ObjectId(), + }) + ).toObject() as unknown as LeanPromptGroup; + + const legacyPrompt = ( + await Prompt.create({ + prompt: 'Legacy prompt without ACL', + name: 'Legacy', + author: testUsers.owner._id, + groupId: promptGroup._id, + type: 'text', + }) + ).toObject() as { _id: mongoose.Types.ObjectId }; + + const prompt = (await methods.getPrompt({ _id: legacyPrompt._id })) as { + _id: mongoose.Types.ObjectId; + } | null; + expect(prompt).toBeTruthy(); + expect(String(prompt!._id)).toBe(String(legacyPrompt._id)); + }); + }); +}); diff --git a/packages/data-schemas/src/methods/prompt.ts b/packages/data-schemas/src/methods/prompt.ts new file mode 100644 index 0000000000..1420495ac2 --- /dev/null +++ b/packages/data-schemas/src/methods/prompt.ts @@ -0,0 +1,691 @@ +import type { Model, Types } from 'mongoose'; +import { SystemRoles, ResourceType, SystemCategories } from 'librechat-data-provider'; +import type { IPrompt, IPromptGroup, IPromptGroupDocument } from '~/types'; +import { escapeRegExp } from '~/utils/string'; +import logger from '~/config/winston'; + +export interface PromptDeps { + /** Removes all ACL permissions for a resource. Injected from PermissionService. */ + removeAllPermissions: (params: { resourceType: string; resourceId: unknown }) => Promise; + /** Returns resource IDs solely owned by the given user. From createAclEntryMethods. */ + getSoleOwnedResourceIds: ( + userObjectId: Types.ObjectId, + resourceTypes: string | string[], + ) => Promise; +} + +export function createPromptMethods(mongoose: typeof import('mongoose'), deps: PromptDeps) { + const { getSoleOwnedResourceIds } = deps; + const { ObjectId } = mongoose.Types; + + /** + * Batch-fetches production prompts for an array of prompt groups + * and attaches them as `productionPrompt` field. + */ + async function attachProductionPrompts( + groups: Array>, + ): Promise>> { + const Prompt = mongoose.models.Prompt as Model; + const uniqueIds = [ + ...new Set(groups.map((g) => (g.productionId as Types.ObjectId)?.toString()).filter(Boolean)), + ]; + if (uniqueIds.length === 0) { + return groups.map((g) => ({ ...g, productionPrompt: null })); + } + + const prompts = await Prompt.find({ _id: { $in: uniqueIds } }) + .select('prompt') + .lean(); + const promptMap = new Map(prompts.map((p) => [p._id.toString(), p])); + + return groups.map((g) => ({ + ...g, + productionPrompt: g.productionId + ? (promptMap.get((g.productionId as Types.ObjectId).toString()) ?? null) + : null, + })); + } + + /** + * Get all prompt groups with filters (no pagination). + */ + async function getAllPromptGroups(filter: Record) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + const { name, ...query } = filter as { + name?: string; + category?: string; + [key: string]: unknown; + }; + + if (name) { + (query as Record).name = new RegExp(escapeRegExp(name), 'i'); + } + if (!query.category) { + delete query.category; + } else if (query.category === SystemCategories.MY_PROMPTS) { + delete query.category; + } else if (query.category === SystemCategories.NO_CATEGORY) { + query.category = ''; + } else if (query.category === SystemCategories.SHARED_PROMPTS) { + delete query.category; + } + + const groups = await PromptGroup.find(query) + .sort({ createdAt: -1 }) + .select('name oneliner category author authorName createdAt updatedAt command productionId') + .lean(); + return await attachProductionPrompts(groups as unknown as Array>); + } catch (error) { + console.error('Error getting all prompt groups', error); + return { message: 'Error getting all prompt groups' }; + } + } + + /** + * Get prompt groups with pagination and filters. + */ + async function getPromptGroups(filter: Record) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + const { + pageNumber = 1, + pageSize = 10, + name, + ...query + } = filter as { + pageNumber?: number | string; + pageSize?: number | string; + name?: string; + category?: string; + [key: string]: unknown; + }; + + const validatedPageNumber = Math.max(parseInt(String(pageNumber), 10), 1); + const validatedPageSize = Math.max(parseInt(String(pageSize), 10), 1); + + if (name) { + (query as Record).name = new RegExp(escapeRegExp(name), 'i'); + } + if (!query.category) { + delete query.category; + } else if (query.category === SystemCategories.MY_PROMPTS) { + delete query.category; + } else if (query.category === SystemCategories.NO_CATEGORY) { + query.category = ''; + } else if (query.category === SystemCategories.SHARED_PROMPTS) { + delete query.category; + } + + const skip = (validatedPageNumber - 1) * validatedPageSize; + const limit = validatedPageSize; + + const [groups, totalPromptGroups] = await Promise.all([ + PromptGroup.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .select( + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', + ) + .lean(), + PromptGroup.countDocuments(query), + ]); + + const promptGroups = await attachProductionPrompts( + groups as unknown as Array>, + ); + + return { + promptGroups, + pageNumber: validatedPageNumber.toString(), + pageSize: validatedPageSize.toString(), + pages: Math.ceil(totalPromptGroups / validatedPageSize).toString(), + }; + } catch (error) { + console.error('Error getting prompt groups', error); + return { message: 'Error getting prompt groups' }; + } + } + + /** + * Delete a prompt group and its prompts, cleaning up ACL permissions. + */ + async function deletePromptGroup({ + _id, + author, + role, + }: { + _id: string; + author?: string; + role?: string; + }) { + const PromptGroup = mongoose.models.PromptGroup as Model; + const Prompt = mongoose.models.Prompt as Model; + + const query: Record = { _id }; + const groupQuery: Record = { groupId: new ObjectId(_id) }; + + if (author && role !== SystemRoles.ADMIN) { + query.author = author; + groupQuery.author = author; + } + + const response = await PromptGroup.deleteOne(query); + + if (!response || response.deletedCount === 0) { + throw new Error('Prompt group not found'); + } + + await Prompt.deleteMany(groupQuery); + + try { + await deps.removeAllPermissions({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: _id, + }); + } catch (error) { + logger.error('Error removing promptGroup permissions:', error); + } + + return { message: 'Prompt group deleted successfully' }; + } + + /** + * Get prompt groups by accessible IDs with optional cursor-based pagination. + */ + async function getListPromptGroupsByAccess({ + accessibleIds = [], + otherParams = {}, + limit = null, + after = null, + }: { + accessibleIds?: Types.ObjectId[]; + otherParams?: Record; + limit?: number | null; + after?: string | null; + }) { + const PromptGroup = mongoose.models.PromptGroup as Model; + const isPaginated = limit !== null && limit !== undefined; + const normalizedLimit = isPaginated + ? Math.min(Math.max(1, parseInt(String(limit)) || 20), 100) + : null; + + const baseQuery: Record = { + ...otherParams, + _id: { $in: accessibleIds }, + }; + + if (after && typeof after === 'string' && after !== 'undefined' && after !== 'null') { + try { + const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8')); + const { updatedAt, _id } = cursor; + + const cursorCondition = { + $or: [ + { updatedAt: { $lt: new Date(updatedAt) } }, + { updatedAt: new Date(updatedAt), _id: { $gt: new ObjectId(_id) } }, + ], + }; + + if (Object.keys(baseQuery).length > 0) { + baseQuery.$and = [{ ...baseQuery }, cursorCondition]; + Object.keys(baseQuery).forEach((key) => { + if (key !== '$and') { + delete baseQuery[key]; + } + }); + } else { + Object.assign(baseQuery, cursorCondition); + } + } catch (error) { + logger.warn('Invalid cursor:', (error as Error).message); + } + } + + const findQuery = PromptGroup.find(baseQuery) + .sort({ updatedAt: -1, _id: 1 }) + .select( + 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt', + ); + + if (isPaginated && normalizedLimit) { + findQuery.limit(normalizedLimit + 1); + } + + const groups = await findQuery.lean(); + const promptGroups = await attachProductionPrompts( + groups as unknown as Array>, + ); + + const hasMore = isPaginated && normalizedLimit ? promptGroups.length > normalizedLimit : false; + const data = ( + isPaginated && normalizedLimit ? promptGroups.slice(0, normalizedLimit) : promptGroups + ).map((group) => { + if (group.author) { + group.author = (group.author as Types.ObjectId).toString(); + } + return group; + }); + + let nextCursor: string | null = null; + if (isPaginated && hasMore && data.length > 0 && normalizedLimit) { + const lastGroup = promptGroups[normalizedLimit - 1] as Record; + nextCursor = Buffer.from( + JSON.stringify({ + updatedAt: (lastGroup.updatedAt as Date).toISOString(), + _id: (lastGroup._id as Types.ObjectId).toString(), + }), + ).toString('base64'); + } + + return { + object: 'list' as const, + data, + first_id: data.length > 0 ? (data[0]._id as Types.ObjectId).toString() : null, + last_id: data.length > 0 ? (data[data.length - 1]._id as Types.ObjectId).toString() : null, + has_more: hasMore, + after: nextCursor, + }; + } + + /** + * Create a prompt and its respective group. + */ + async function createPromptGroup(saveData: { + prompt: Record; + group: Record; + author: string; + authorName: string; + }) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + const Prompt = mongoose.models.Prompt as Model; + const { prompt, group, author, authorName } = saveData; + + let newPromptGroup = await PromptGroup.findOneAndUpdate( + { ...group, author, authorName, productionId: null }, + { $setOnInsert: { ...group, author, authorName, productionId: null } }, + { new: true, upsert: true }, + ) + .lean() + .select('-__v') + .exec(); + + const newPrompt = await Prompt.findOneAndUpdate( + { ...prompt, author, groupId: newPromptGroup!._id }, + { $setOnInsert: { ...prompt, author, groupId: newPromptGroup!._id } }, + { new: true, upsert: true }, + ) + .lean() + .select('-__v') + .exec(); + + newPromptGroup = (await PromptGroup.findByIdAndUpdate( + newPromptGroup!._id, + { productionId: newPrompt!._id }, + { new: true }, + ) + .lean() + .select('-__v') + .exec())!; + + return { + prompt: newPrompt, + group: { + ...newPromptGroup, + productionPrompt: { prompt: (newPrompt as unknown as IPrompt).prompt }, + }, + }; + } catch (error) { + logger.error('Error saving prompt group', error); + throw new Error('Error saving prompt group'); + } + } + + /** + * Save a prompt. + */ + async function savePrompt(saveData: { + prompt: Record; + author: string | Types.ObjectId; + }) { + try { + const Prompt = mongoose.models.Prompt as Model; + const { prompt, author } = saveData; + const newPromptData = { ...prompt, author }; + + let newPrompt; + try { + newPrompt = await Prompt.create(newPromptData); + } catch (error: unknown) { + if ((error as Error)?.message?.includes('groupId_1_version_1')) { + await Prompt.db.collection('prompts').dropIndex('groupId_1_version_1'); + } else { + throw error; + } + newPrompt = await Prompt.create(newPromptData); + } + + return { prompt: newPrompt }; + } catch (error) { + logger.error('Error saving prompt', error); + return { message: 'Error saving prompt' }; + } + } + + /** + * Get prompts by filter. + */ + async function getPrompts(filter: Record) { + try { + const Prompt = mongoose.models.Prompt as Model; + return await Prompt.find(filter).sort({ createdAt: -1 }).lean(); + } catch (error) { + logger.error('Error getting prompts', error); + return { message: 'Error getting prompts' }; + } + } + + /** + * Get a single prompt by filter. + */ + async function getPrompt(filter: Record) { + try { + const Prompt = mongoose.models.Prompt as Model; + if (filter.groupId) { + filter.groupId = new ObjectId(filter.groupId as string); + } + return await Prompt.findOne(filter).lean(); + } catch (error) { + logger.error('Error getting prompt', error); + return { message: 'Error getting prompt' }; + } + } + + /** + * Get random prompt groups from distinct categories. + */ + async function getRandomPromptGroups(filter: { skip: number | string; limit: number | string }) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + const categories = await PromptGroup.distinct('category', { category: { $ne: '' } }); + + for (let i = categories.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [categories[i], categories[j]] = [categories[j], categories[i]]; + } + + const skip = +filter.skip; + const limit = +filter.limit; + const selectedCategories = categories.slice(skip, skip + limit); + + if (selectedCategories.length === 0) { + return { prompts: [] }; + } + + const groups = await PromptGroup.find({ category: { $in: selectedCategories } }).lean(); + + const groupByCategory = new Map(); + for (const group of groups) { + if (!groupByCategory.has(group.category)) { + groupByCategory.set(group.category, group); + } + } + + const prompts = selectedCategories + .map((cat: string) => groupByCategory.get(cat)) + .filter(Boolean); + + return { prompts }; + } catch (error) { + logger.error('Error getting prompt groups', error); + return { message: 'Error getting prompt groups' }; + } + } + + /** + * Get prompt groups with populated prompts. + */ + async function getPromptGroupsWithPrompts(filter: Record) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + return await PromptGroup.findOne(filter) + .populate({ + path: 'prompts', + select: '-_id -__v -user', + }) + .select('-_id -__v -user') + .lean(); + } catch (error) { + logger.error('Error getting prompt groups', error); + return { message: 'Error getting prompt groups' }; + } + } + + /** + * Get a single prompt group by filter. + */ + async function getPromptGroup(filter: Record) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + return await PromptGroup.findOne(filter).lean(); + } catch (error) { + logger.error('Error getting prompt group', error); + return { message: 'Error getting prompt group' }; + } + } + + /** + * Delete a prompt, potentially removing the group if it's the last prompt. + */ + async function deletePrompt({ + promptId, + groupId, + author, + role, + }: { + promptId: string | Types.ObjectId; + groupId: string | Types.ObjectId; + author: string | Types.ObjectId; + role?: string; + }) { + const Prompt = mongoose.models.Prompt as Model; + const PromptGroup = mongoose.models.PromptGroup as Model; + + const query: Record = { _id: promptId, groupId, author }; + if (role === SystemRoles.ADMIN) { + delete query.author; + } + const { deletedCount } = await Prompt.deleteOne(query); + if (deletedCount === 0) { + throw new Error('Failed to delete the prompt'); + } + + const remainingPrompts = await Prompt.find({ groupId }) + .select('_id') + .sort({ createdAt: 1 }) + .lean(); + + if (remainingPrompts.length === 0) { + try { + await deps.removeAllPermissions({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: groupId, + }); + } catch (error) { + logger.error('Error removing promptGroup permissions:', error); + } + + await PromptGroup.deleteOne({ _id: groupId }); + + return { + prompt: 'Prompt deleted successfully', + promptGroup: { + message: 'Prompt group deleted successfully', + id: groupId, + }, + }; + } else { + const promptGroup = (await PromptGroup.findById( + groupId, + ).lean()) as unknown as IPromptGroup | null; + if (promptGroup && promptGroup.productionId?.toString() === promptId.toString()) { + await PromptGroup.updateOne( + { _id: groupId }, + { productionId: remainingPrompts[remainingPrompts.length - 1]._id }, + ); + } + + return { prompt: 'Prompt deleted successfully' }; + } + } + + /** + * Delete all prompts and prompt groups created by a specific user. + */ + /** + * Deletes prompt groups solely owned by the user and cleans up their prompts/ACLs. + * Groups with other owners are left intact; the caller is responsible for + * removing the user's own ACL principal entries separately. + * + * Also handles legacy (pre-ACL) prompt groups that only have the author field set, + * ensuring they are not orphaned if the permission migration has not been run. + */ + async function deleteUserPrompts(userId: string) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + const Prompt = mongoose.models.Prompt as Model; + const AclEntry = mongoose.models.AclEntry; + + const userObjectId = new ObjectId(userId); + const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.PROMPTGROUP); + + const authoredGroups = await PromptGroup.find({ author: userObjectId }).select('_id').lean(); + const authoredGroupIds = authoredGroups.map((g) => g._id); + + const migratedEntries = + authoredGroupIds.length > 0 + ? await AclEntry.find({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: { $in: authoredGroupIds }, + }) + .select('resourceId') + .lean() + : []; + const migratedIds = new Set( + (migratedEntries as Array<{ resourceId: Types.ObjectId }>).map((e) => e.resourceId.toString()), + ); + const legacyGroupIds = authoredGroupIds.filter( + (id) => !migratedIds.has(id.toString()), + ); + + const allGroupIdsToDelete = [...soleOwnedIds, ...legacyGroupIds]; + + if (allGroupIdsToDelete.length === 0) { + return; + } + + await AclEntry.deleteMany({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: { $in: allGroupIdsToDelete }, + }); + + await PromptGroup.deleteMany({ _id: { $in: allGroupIdsToDelete } }); + await Prompt.deleteMany({ groupId: { $in: allGroupIdsToDelete } }); + } catch (error) { + logger.error('[deleteUserPrompts] General error:', error); + } + } + + /** + * Update a prompt group. + */ + async function updatePromptGroup(filter: Record, data: Record) { + try { + const PromptGroup = mongoose.models.PromptGroup as Model; + const updateOps = {}; + const updateData = { ...data, ...updateOps }; + const updatedDoc = await PromptGroup.findOneAndUpdate(filter, updateData, { + new: true, + upsert: false, + }); + + if (!updatedDoc) { + throw new Error('Prompt group not found'); + } + + return updatedDoc; + } catch (error) { + logger.error('Error updating prompt group', error); + return { message: 'Error updating prompt group' }; + } + } + + /** + * Make a prompt the production prompt for its group. + */ + async function makePromptProduction(promptId: string) { + try { + const Prompt = mongoose.models.Prompt as Model; + const PromptGroup = mongoose.models.PromptGroup as Model; + + const prompt = await Prompt.findById(promptId).lean(); + + if (!prompt) { + throw new Error('Prompt not found'); + } + + await PromptGroup.findByIdAndUpdate( + prompt.groupId, + { productionId: prompt._id }, + { new: true }, + ) + .lean() + .exec(); + + return { message: 'Prompt production made successfully' }; + } catch (error) { + logger.error('Error making prompt production', error); + return { message: 'Error making prompt production' }; + } + } + + /** + * Update prompt labels. + */ + async function updatePromptLabels(_id: string, labels: unknown) { + try { + const Prompt = mongoose.models.Prompt as Model; + const response = await Prompt.updateOne({ _id }, { $set: { labels } }); + if (response.matchedCount === 0) { + return { message: 'Prompt not found' }; + } + return { message: 'Prompt labels updated successfully' }; + } catch (error) { + logger.error('Error updating prompt labels', error); + return { message: 'Error updating prompt labels' }; + } + } + + return { + getPromptGroups, + deletePromptGroup, + getAllPromptGroups, + getListPromptGroupsByAccess, + createPromptGroup, + savePrompt, + getPrompts, + getPrompt, + getRandomPromptGroups, + getPromptGroupsWithPrompts, + getPromptGroup, + deletePrompt, + deleteUserPrompts, + updatePromptGroup, + makePromptProduction, + updatePromptLabels, + }; +} + +export type PromptMethods = ReturnType; diff --git a/api/models/Role.spec.js b/packages/data-schemas/src/methods/role.methods.spec.ts similarity index 87% rename from api/models/Role.spec.js rename to packages/data-schemas/src/methods/role.methods.spec.ts index 0ec2f831e2..78d7f98ea1 100644 --- a/api/models/Role.spec.js +++ b/packages/data-schemas/src/methods/role.methods.spec.ts @@ -1,31 +1,34 @@ -const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { - SystemRoles, - Permissions, - roleDefaults, - PermissionTypes, -} = require('librechat-data-provider'); -const { getRoleByName, updateAccessPermissions } = require('~/models/Role'); -const getLogStores = require('~/cache/getLogStores'); -const { initializeRoles } = require('~/models'); -const { Role } = require('~/db/models'); +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { SystemRoles, Permissions, roleDefaults, PermissionTypes } from 'librechat-data-provider'; +import type { IRole, RolePermissions } from '..'; +import { createRoleMethods } from './role'; +import { createModels } from '../models'; -// Mock the cache -jest.mock('~/cache/getLogStores', () => - jest.fn().mockReturnValue({ - get: jest.fn(), - set: jest.fn(), - del: jest.fn(), - }), -); +const mockCache = { + get: jest.fn(), + set: jest.fn(), + del: jest.fn(), +}; -let mongoServer; +const mockGetCache = jest.fn().mockReturnValue(mockCache); + +let Role: mongoose.Model; +let getRoleByName: ReturnType['getRoleByName']; +let updateAccessPermissions: ReturnType['updateAccessPermissions']; +let initializeRoles: ReturnType['initializeRoles']; +let mongoServer: MongoMemoryServer; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); await mongoose.connect(mongoUri); + createModels(mongoose); + Role = mongoose.models.Role; + const methods = createRoleMethods(mongoose, { getCache: mockGetCache }); + getRoleByName = methods.getRoleByName; + updateAccessPermissions = methods.updateAccessPermissions; + initializeRoles = methods.initializeRoles; }); afterAll(async () => { @@ -35,7 +38,10 @@ afterAll(async () => { beforeEach(async () => { await Role.deleteMany({}); - getLogStores.mockClear(); + mockGetCache.mockClear(); + mockCache.get.mockClear(); + mockCache.set.mockClear(); + mockCache.del.mockClear(); }); describe('updateAccessPermissions', () => { @@ -377,9 +383,9 @@ describe('initializeRoles', () => { }); // Example: Check default values for ADMIN role - expect(adminRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true); - expect(adminRole.permissions[PermissionTypes.BOOKMARKS].USE).toBe(true); - expect(adminRole.permissions[PermissionTypes.AGENTS].CREATE).toBe(true); + expect(adminRole.permissions[PermissionTypes.PROMPTS]?.SHARE).toBe(true); + expect(adminRole.permissions[PermissionTypes.BOOKMARKS]?.USE).toBe(true); + expect(adminRole.permissions[PermissionTypes.AGENTS]?.CREATE).toBe(true); }); it('should not modify existing permissions for existing roles', async () => { @@ -424,9 +430,9 @@ describe('initializeRoles', () => { const userRole = await getRoleByName(SystemRoles.USER); expect(userRole.permissions[PermissionTypes.AGENTS]).toBeDefined(); - expect(userRole.permissions[PermissionTypes.AGENTS].CREATE).toBeDefined(); - expect(userRole.permissions[PermissionTypes.AGENTS].USE).toBeDefined(); - expect(userRole.permissions[PermissionTypes.AGENTS].SHARE).toBeDefined(); + expect(userRole.permissions[PermissionTypes.AGENTS]?.CREATE).toBeDefined(); + expect(userRole.permissions[PermissionTypes.AGENTS]?.USE).toBeDefined(); + expect(userRole.permissions[PermissionTypes.AGENTS]?.SHARE).toBeDefined(); }); it('should handle multiple runs without duplicating or modifying data', async () => { @@ -439,8 +445,8 @@ describe('initializeRoles', () => { expect(adminRoles).toHaveLength(1); expect(userRoles).toHaveLength(1); - const adminPerms = adminRoles[0].toObject().permissions; - const userPerms = userRoles[0].toObject().permissions; + const adminPerms = adminRoles[0].toObject().permissions as RolePermissions; + const userPerms = userRoles[0].toObject().permissions as RolePermissions; Object.values(PermissionTypes).forEach((permType) => { expect(adminPerms[permType]).toBeDefined(); expect(userPerms[permType]).toBeDefined(); @@ -469,9 +475,9 @@ describe('initializeRoles', () => { partialAdminRole.permissions[PermissionTypes.PROMPTS], ); expect(adminRole.permissions[PermissionTypes.AGENTS]).toBeDefined(); - expect(adminRole.permissions[PermissionTypes.AGENTS].CREATE).toBeDefined(); - expect(adminRole.permissions[PermissionTypes.AGENTS].USE).toBeDefined(); - expect(adminRole.permissions[PermissionTypes.AGENTS].SHARE).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.AGENTS]?.CREATE).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.AGENTS]?.USE).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.AGENTS]?.SHARE).toBeDefined(); }); it('should include MULTI_CONVO permissions when creating default roles', async () => { @@ -482,10 +488,10 @@ describe('initializeRoles', () => { expect(adminRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined(); expect(userRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined(); - expect(adminRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe( + expect(adminRole.permissions[PermissionTypes.MULTI_CONVO]?.USE).toBe( roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.MULTI_CONVO].USE, ); - expect(userRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe( + expect(userRole.permissions[PermissionTypes.MULTI_CONVO]?.USE).toBe( roleDefaults[SystemRoles.USER].permissions[PermissionTypes.MULTI_CONVO].USE, ); }); @@ -506,6 +512,6 @@ describe('initializeRoles', () => { const userRole = await getRoleByName(SystemRoles.USER); expect(userRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined(); - expect(userRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBeDefined(); + expect(userRole.permissions[PermissionTypes.MULTI_CONVO]?.USE).toBeDefined(); }); }); diff --git a/packages/data-schemas/src/methods/role.ts b/packages/data-schemas/src/methods/role.ts index a12c5fafe5..7b51e45330 100644 --- a/packages/data-schemas/src/methods/role.ts +++ b/packages/data-schemas/src/methods/role.ts @@ -1,7 +1,22 @@ -import { roleDefaults, SystemRoles } from 'librechat-data-provider'; +import { + CacheKeys, + SystemRoles, + roleDefaults, + permissionsSchema, + removeNullishValues, +} from 'librechat-data-provider'; +import type { IRole } from '~/types'; +import logger from '~/config/winston'; -// Factory function that takes mongoose instance and returns the methods -export function createRoleMethods(mongoose: typeof import('mongoose')) { +export interface RoleDeps { + /** Returns a cache store for the given key. Injected from getLogStores. */ + getCache?: (key: string) => { + get: (k: string) => Promise; + set: (k: string, v: unknown) => Promise; + }; +} + +export function createRoleMethods(mongoose: typeof import('mongoose'), deps: RoleDeps = {}) { /** * Initialize default roles in the system. * Creates the default roles (ADMIN, USER) if they don't exist in the database. @@ -30,18 +45,310 @@ export function createRoleMethods(mongoose: typeof import('mongoose')) { } /** - * List all roles in the system (for testing purposes) - * Returns an array of all roles with their names and permissions + * List all roles in the system. */ async function listRoles() { const Role = mongoose.models.Role; return await Role.find({}).select('name permissions').lean(); } - // Return all methods you want to expose + /** + * Retrieve a role by name and convert the found role document to a plain object. + * If the role with the given name doesn't exist and the name is a system defined role, + * create it and return the lean version. + */ + async function getRoleByName(roleName: string, fieldsToSelect: string | string[] | null = null) { + const cache = deps.getCache?.(CacheKeys.ROLES); + try { + if (cache) { + const cachedRole = await cache.get(roleName); + if (cachedRole) { + return cachedRole as IRole; + } + } + const Role = mongoose.models.Role; + let query = Role.findOne({ name: roleName }); + if (fieldsToSelect) { + query = query.select(fieldsToSelect); + } + const role = await query.lean().exec(); + + if (!role && SystemRoles[roleName as keyof typeof SystemRoles]) { + const newRole = await new Role(roleDefaults[roleName as keyof typeof roleDefaults]).save(); + if (cache) { + await cache.set(roleName, newRole); + } + return newRole.toObject() as IRole; + } + if (cache) { + await cache.set(roleName, role); + } + return role as unknown as IRole; + } catch (error) { + throw new Error(`Failed to retrieve or create role: ${(error as Error).message}`); + } + } + + /** + * Update role values by name. + */ + async function updateRoleByName(roleName: string, updates: Partial) { + const cache = deps.getCache?.(CacheKeys.ROLES); + try { + const Role = mongoose.models.Role; + const role = await Role.findOneAndUpdate( + { name: roleName }, + { $set: updates }, + { new: true, lean: true }, + ) + .select('-__v') + .lean() + .exec(); + if (cache) { + await cache.set(roleName, role); + } + return role as unknown as IRole; + } catch (error) { + throw new Error(`Failed to update role: ${(error as Error).message}`); + } + } + + /** + * Updates access permissions for a specific role and multiple permission types. + */ + async function updateAccessPermissions( + roleName: string, + permissionsUpdate: Record>, + roleData?: IRole, + ) { + const updates: Record> = {}; + for (const [permissionType, permissions] of Object.entries(permissionsUpdate)) { + if ( + permissionsSchema.shape && + permissionsSchema.shape[permissionType as keyof typeof permissionsSchema.shape] + ) { + updates[permissionType] = removeNullishValues(permissions) as Record; + } + } + if (!Object.keys(updates).length) { + return; + } + + try { + const role = roleData ?? (await getRoleByName(roleName)); + if (!role) { + return; + } + + const currentPermissions = + ((role as unknown as Record).permissions as Record< + string, + Record + >) || {}; + const updatedPermissions: Record> = { ...currentPermissions }; + let hasChanges = false; + + const unsetFields: Record = {}; + const permissionTypes = Object.keys(permissionsSchema.shape || {}); + for (const permType of permissionTypes) { + if ( + (role as unknown as Record)[permType] && + typeof (role as unknown as Record)[permType] === 'object' + ) { + logger.info( + `Migrating '${roleName}' role from old schema: found '${permType}' at top level`, + ); + + updatedPermissions[permType] = { + ...updatedPermissions[permType], + ...((role as unknown as Record)[permType] as Record), + }; + + unsetFields[permType] = 1; + hasChanges = true; + } + } + + // Migrate legacy SHARED_GLOBAL → SHARE for PROMPTS and AGENTS. + // SHARED_GLOBAL was removed in favour of SHARE in PR #11283. If the DB still has + // SHARED_GLOBAL but not SHARE, inherit the value so sharing intent is preserved. + const legacySharedGlobalTypes = ['PROMPTS', 'AGENTS']; + for (const legacyPermType of legacySharedGlobalTypes) { + const existingTypePerms = currentPermissions[legacyPermType]; + if ( + existingTypePerms && + 'SHARED_GLOBAL' in existingTypePerms && + !('SHARE' in existingTypePerms) && + updates[legacyPermType] && + // Don't override an explicit SHARE value the caller already provided + !('SHARE' in updates[legacyPermType]) + ) { + const inheritedValue = existingTypePerms['SHARED_GLOBAL']; + updates[legacyPermType]['SHARE'] = inheritedValue; + logger.info( + `Migrating '${roleName}' role ${legacyPermType}.SHARED_GLOBAL=${inheritedValue} → SHARE`, + ); + } + } + + for (const [permissionType, permissions] of Object.entries(updates)) { + const currentTypePermissions = currentPermissions[permissionType] || {}; + updatedPermissions[permissionType] = { ...currentTypePermissions }; + + for (const [permission, value] of Object.entries(permissions)) { + if (currentTypePermissions[permission] !== value) { + updatedPermissions[permissionType][permission] = value; + hasChanges = true; + logger.info( + `Updating '${roleName}' role permission '${permissionType}' '${permission}' from ${currentTypePermissions[permission]} to: ${value}`, + ); + } + } + } + + // Clean up orphaned SHARED_GLOBAL fields left in DB after the schema rename. + // Since we $set the full permissions object, deleting from updatedPermissions + // is sufficient to remove the field from MongoDB. + for (const legacyPermType of legacySharedGlobalTypes) { + const existingTypePerms = currentPermissions[legacyPermType]; + if (existingTypePerms && 'SHARED_GLOBAL' in existingTypePerms) { + if (!updates[legacyPermType]) { + // permType wasn't in the update payload so the migration block above didn't run. + // Create a writable copy and handle the SHARED_GLOBAL → SHARE inheritance here + // to avoid removing SHARED_GLOBAL without writing SHARE (data loss). + updatedPermissions[legacyPermType] = { ...existingTypePerms }; + if (!('SHARE' in existingTypePerms)) { + updatedPermissions[legacyPermType]['SHARE'] = existingTypePerms['SHARED_GLOBAL']; + logger.info( + `Migrating '${roleName}' role ${legacyPermType}.SHARED_GLOBAL=${existingTypePerms['SHARED_GLOBAL']} → SHARE`, + ); + } + } + delete updatedPermissions[legacyPermType]['SHARED_GLOBAL']; + hasChanges = true; + logger.info( + `Removed legacy SHARED_GLOBAL field from '${roleName}' role ${legacyPermType} permissions`, + ); + } + } + + if (hasChanges) { + const Role = mongoose.models.Role; + const updateObj = { permissions: updatedPermissions }; + + if (Object.keys(unsetFields).length > 0) { + logger.info( + `Unsetting old schema fields for '${roleName}' role: ${Object.keys(unsetFields).join(', ')}`, + ); + + try { + await Role.updateOne( + { name: roleName }, + { + $set: updateObj, + $unset: unsetFields, + }, + ); + + const cache = deps.getCache?.(CacheKeys.ROLES); + const updatedRole = await Role.findOne({ name: roleName }).select('-__v').lean().exec(); + if (cache) { + await cache.set(roleName, updatedRole); + } + + logger.info(`Updated role '${roleName}' and removed old schema fields`); + } catch (updateError) { + logger.error(`Error during role migration update: ${(updateError as Error).message}`); + throw updateError; + } + } else { + await updateRoleByName(roleName, updateObj as unknown as Partial); + } + + logger.info(`Updated '${roleName}' role permissions`); + } else { + logger.info(`No changes needed for '${roleName}' role permissions`); + } + } catch (error) { + logger.error(`Failed to update ${roleName} role permissions:`, error); + } + } + + /** + * Migrates roles from old schema to new schema structure. + */ + async function migrateRoleSchema(roleName?: string): Promise { + try { + const Role = mongoose.models.Role; + let roles; + if (roleName) { + const role = await Role.findOne({ name: roleName }); + roles = role ? [role] : []; + } else { + roles = await Role.find({}); + } + + logger.info(`Migrating ${roles.length} roles to new schema structure`); + let migratedCount = 0; + + for (const role of roles) { + const permissionTypes = Object.keys(permissionsSchema.shape || {}); + const unsetFields: Record = {}; + let hasOldSchema = false; + + for (const permType of permissionTypes) { + if (role[permType] && typeof role[permType] === 'object') { + hasOldSchema = true; + role.permissions = role.permissions || {}; + role.permissions[permType] = { + ...role.permissions[permType], + ...role[permType], + }; + unsetFields[permType] = 1; + } + } + + if (hasOldSchema) { + try { + logger.info(`Migrating role '${role.name}' from old schema structure`); + + await Role.updateOne( + { _id: role._id }, + { + $set: { permissions: role.permissions }, + $unset: unsetFields, + }, + ); + + const cache = deps.getCache?.(CacheKeys.ROLES); + if (cache) { + const updatedRole = await Role.findById(role._id).lean().exec(); + await cache.set(role.name, updatedRole); + } + + migratedCount++; + logger.info(`Migrated role '${role.name}'`); + } catch (error) { + logger.error(`Failed to migrate role '${role.name}': ${(error as Error).message}`); + } + } + } + + logger.info(`Migration complete: ${migratedCount} roles migrated`); + return migratedCount; + } catch (error) { + logger.error(`Role schema migration failed: ${(error as Error).message}`); + throw error; + } + } + return { listRoles, initializeRoles, + getRoleByName, + updateRoleByName, + updateAccessPermissions, + migrateRoleSchema, }; } diff --git a/api/models/spendTokens.spec.js b/packages/data-schemas/src/methods/spendTokens.spec.ts similarity index 87% rename from api/models/spendTokens.spec.js rename to packages/data-schemas/src/methods/spendTokens.spec.ts index dfeec5ee83..58e5f4a0ab 100644 --- a/api/models/spendTokens.spec.js +++ b/packages/data-schemas/src/methods/spendTokens.spec.ts @@ -1,30 +1,60 @@ -const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { createTransaction, createAutoRefillTransaction } = require('./Transaction'); -const { tokenValues, premiumTokenValues, getCacheMultiplier } = require('./tx'); -const { spendTokens, spendStructuredTokens } = require('./spendTokens'); +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { matchModelName, findMatchingPattern } from './test-helpers'; +import { createModels } from '~/models'; +import { createTxMethods, tokenValues, premiumTokenValues } from './tx'; +import { createTransactionMethods } from './transaction'; +import { createSpendTokensMethods } from './spendTokens'; +import type { ITransaction } from '~/schema/transaction'; +import type { IBalance } from '..'; -require('~/db/models'); - -jest.mock('~/config', () => ({ - logger: { - debug: jest.fn(), - error: jest.fn(), - }, +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), })); +let mongoServer: InstanceType; +let spendTokens: ReturnType['spendTokens']; +let spendStructuredTokens: ReturnType['spendStructuredTokens']; +let createTransaction: ReturnType['createTransaction']; +let createAutoRefillTransaction: ReturnType< + typeof createTransactionMethods +>['createAutoRefillTransaction']; +let getCacheMultiplier: ReturnType['getCacheMultiplier']; + describe('spendTokens', () => { - let mongoServer; - let userId; - let Transaction; - let Balance; + let userId: mongoose.Types.ObjectId; + let Transaction: mongoose.Model; + let Balance: mongoose.Model; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); await mongoose.connect(mongoServer.getUri()); - Transaction = mongoose.model('Transaction'); - Balance = mongoose.model('Balance'); + const models = createModels(mongoose); + Object.assign(mongoose.models, models); + + Transaction = mongoose.models.Transaction; + Balance = mongoose.models.Balance; + + const txMethods = createTxMethods(mongoose, { matchModelName, findMatchingPattern }); + getCacheMultiplier = txMethods.getCacheMultiplier; + + const transactionMethods = createTransactionMethods(mongoose, { + getMultiplier: txMethods.getMultiplier, + getCacheMultiplier: txMethods.getCacheMultiplier, + }); + createTransaction = transactionMethods.createTransaction; + createAutoRefillTransaction = transactionMethods.createAutoRefillTransaction; + + const spendMethods = createSpendTokensMethods(mongoose, { + createTransaction: transactionMethods.createTransaction, + createStructuredTransaction: transactionMethods.createStructuredTransaction, + }); + spendTokens = spendMethods.spendTokens; + spendStructuredTokens = spendMethods.spendStructuredTokens; }); afterAll(async () => { @@ -79,7 +109,7 @@ describe('spendTokens', () => { // Verify balance was updated const balance = await Balance.findOne({ user: userId }); expect(balance).toBeDefined(); - expect(balance.tokenCredits).toBeLessThan(10000); // Balance should be reduced + expect(balance!.tokenCredits).toBeLessThan(10000); // Balance should be reduced }); it('should handle zero completion tokens', async () => { @@ -111,7 +141,7 @@ describe('spendTokens', () => { expect(transactions[0].tokenType).toBe('completion'); // In JavaScript -0 and 0 are different but functionally equivalent // Use Math.abs to handle both 0 and -0 - expect(Math.abs(transactions[0].rawAmount)).toBe(0); + expect(Math.abs(transactions[0].rawAmount!)).toBe(0); // Check prompt transaction expect(transactions[1].tokenType).toBe('prompt'); @@ -163,7 +193,7 @@ describe('spendTokens', () => { // Verify balance was not updated (should still be 10000) const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(10000); + expect(balance!.tokenCredits).toBe(10000); }); it('should not allow balance to go below zero when spending tokens', async () => { @@ -196,7 +226,7 @@ describe('spendTokens', () => { // Verify balance was reduced to exactly 0, not negative const balance = await Balance.findOne({ user: userId }); expect(balance).toBeDefined(); - expect(balance.tokenCredits).toBe(0); + expect(balance!.tokenCredits).toBe(0); // Check that the transaction records show the adjusted values const transactionResults = await Promise.all( @@ -244,7 +274,7 @@ describe('spendTokens', () => { // Check balance after first transaction let balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(0); + expect(balance!.tokenCredits).toBe(0); // Second transaction - should keep balance at 0, not make it negative or increase it const txData2 = { @@ -264,7 +294,7 @@ describe('spendTokens', () => { // Check balance after second transaction - should still be 0 balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(0); + expect(balance!.tokenCredits).toBe(0); // Verify all transactions were created const transactions = await Transaction.find({ user: userId }); @@ -275,7 +305,7 @@ describe('spendTokens', () => { // Log the transaction details for debugging console.log('Transaction details:'); - transactionDetails.forEach((tx, i) => { + transactionDetails.forEach((tx, i: number) => { console.log(`Transaction ${i + 1}:`, { tokenType: tx.tokenType, rawAmount: tx.rawAmount, @@ -299,7 +329,7 @@ describe('spendTokens', () => { console.log('Direct Transaction.create result:', directResult); // The completion value should never be positive - expect(directResult.completion).not.toBeGreaterThan(0); + expect(directResult!.completion).not.toBeGreaterThan(0); }); it('should ensure tokenValue is always negative for spending tokens', async () => { @@ -371,7 +401,7 @@ describe('spendTokens', () => { // Check balance after first transaction let balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(0); + expect(balance!.tokenCredits).toBe(0); // Second transaction - should keep balance at 0, not make it negative or increase it const txData2 = { @@ -395,7 +425,7 @@ describe('spendTokens', () => { // Check balance after second transaction - should still be 0 balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(0); + expect(balance!.tokenCredits).toBe(0); // Verify all transactions were created const transactions = await Transaction.find({ user: userId }); @@ -406,7 +436,7 @@ describe('spendTokens', () => { // Log the transaction details for debugging console.log('Structured transaction details:'); - transactionDetails.forEach((tx, i) => { + transactionDetails.forEach((tx, i: number) => { console.log(`Transaction ${i + 1}:`, { tokenType: tx.tokenType, rawAmount: tx.rawAmount, @@ -453,7 +483,7 @@ describe('spendTokens', () => { // Verify balance was reduced to exactly 0, not negative const balance = await Balance.findOne({ user: userId }); expect(balance).toBeDefined(); - expect(balance.tokenCredits).toBe(0); + expect(balance!.tokenCredits).toBe(0); // The result should show the adjusted values expect(result).toEqual({ @@ -494,7 +524,7 @@ describe('spendTokens', () => { })); // Process all transactions concurrently to simulate race conditions - const promises = []; + const promises: Promise[] = []; let expectedTotalSpend = 0; for (let i = 0; i < collectedUsage.length; i++) { @@ -567,10 +597,10 @@ describe('spendTokens', () => { console.log('Initial balance:', initialBalance); console.log('Expected total spend:', expectedTotalSpend); console.log('Expected final balance:', expectedFinalBalance); - console.log('Actual final balance:', finalBalance.tokenCredits); + console.log('Actual final balance:', finalBalance!.tokenCredits); // Allow for small rounding differences - expect(finalBalance.tokenCredits).toBeCloseTo(expectedFinalBalance, 0); + expect(finalBalance!.tokenCredits).toBeCloseTo(expectedFinalBalance, 0); // Verify all transactions were created const transactions = await Transaction.find({ @@ -587,19 +617,19 @@ describe('spendTokens', () => { let totalTokenValue = 0; transactions.forEach((tx) => { console.log(`${tx.tokenType}: rawAmount=${tx.rawAmount}, tokenValue=${tx.tokenValue}`); - totalTokenValue += tx.tokenValue; + totalTokenValue += tx.tokenValue!; }); console.log('Total token value from transactions:', totalTokenValue); // The difference between expected and actual is significant // This is likely due to the multipliers being different in the test environment // Let's adjust our expectation based on the actual transactions - const actualSpend = initialBalance - finalBalance.tokenCredits; + const actualSpend = initialBalance - finalBalance!.tokenCredits; console.log('Actual spend:', actualSpend); // Instead of checking the exact balance, let's verify that: // 1. The balance was reduced (tokens were spent) - expect(finalBalance.tokenCredits).toBeLessThan(initialBalance); + expect(finalBalance!.tokenCredits).toBeLessThan(initialBalance); // 2. The total token value from transactions matches the actual spend expect(Math.abs(totalTokenValue)).toBeCloseTo(actualSpend, -3); // Allow for larger differences }); @@ -616,7 +646,7 @@ describe('spendTokens', () => { const numberOfRefills = 25; const refillAmount = 1000; - const promises = []; + const promises: Promise[] = []; for (let i = 0; i < numberOfRefills; i++) { promises.push( createAutoRefillTransaction({ @@ -642,10 +672,10 @@ describe('spendTokens', () => { console.log('Initial balance (Increase Test):', initialBalance); console.log(`Performed ${numberOfRefills} refills of ${refillAmount} each.`); console.log('Expected final balance (Increase Test):', expectedFinalBalance); - console.log('Actual final balance (Increase Test):', finalBalance.tokenCredits); + console.log('Actual final balance (Increase Test):', finalBalance!.tokenCredits); // Use toBeCloseTo for safety, though toBe should work for integer math - expect(finalBalance.tokenCredits).toBeCloseTo(expectedFinalBalance, 0); + expect(finalBalance!.tokenCredits).toBeCloseTo(expectedFinalBalance, 0); // Verify all transactions were created const transactions = await Transaction.find({ @@ -657,12 +687,13 @@ describe('spendTokens', () => { expect(transactions.length).toBe(numberOfRefills); // Optional: Verify the sum of increments from the results matches the balance change - const totalIncrementReported = results.reduce((sum, result) => { + const totalIncrementReported = results.reduce((sum: number, result) => { // Assuming createAutoRefillTransaction returns an object with the increment amount // Adjust this based on the actual return structure. // Let's assume it returns { balance: newBalance, transaction: { rawAmount: ... } } // Or perhaps we check the transaction.rawAmount directly - return sum + (result?.transaction?.rawAmount || 0); + const r = result as Record>; + return sum + ((r?.transaction?.rawAmount as number) || 0); }, 0); console.log('Total increment reported by results:', totalIncrementReported); expect(totalIncrementReported).toBe(expectedFinalBalance - initialBalance); @@ -673,7 +704,7 @@ describe('spendTokens', () => { // For refills, rawAmount is positive, and tokenValue might be calculated based on it // Let's assume tokenValue directly reflects the increment for simplicity here // If calculation is involved, adjust accordingly - totalTokenValueFromDb += tx.rawAmount; // Or tx.tokenValue if that holds the increment + totalTokenValueFromDb += tx.rawAmount!; // Or tx.tokenValue if that holds the increment }); console.log('Total rawAmount from DB transactions:', totalTokenValueFromDb); expect(totalTokenValueFromDb).toBeCloseTo(expectedFinalBalance - initialBalance, 0); @@ -733,7 +764,7 @@ describe('spendTokens', () => { // Verify balance was updated const balance = await Balance.findOne({ user: userId }); expect(balance).toBeDefined(); - expect(balance.tokenCredits).toBeLessThan(10000); // Balance should be reduced + expect(balance!.tokenCredits).toBeLessThan(10000); // Balance should be reduced }); describe('premium token pricing', () => { @@ -762,7 +793,7 @@ describe('spendTokens', () => { promptTokens * tokenValues[model].prompt + completionTokens * tokenValues[model].completion; const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(balance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); it('should charge premium rates for claude-opus-4-6 when prompt tokens exceed threshold', async () => { @@ -791,7 +822,7 @@ describe('spendTokens', () => { completionTokens * premiumTokenValues[model].completion; const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(balance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); it('should charge premium rates for both prompt and completion in structured tokens when above threshold', async () => { @@ -828,12 +859,12 @@ describe('spendTokens', () => { const expectedPromptCost = tokenUsage.promptTokens.input * premiumPromptRate + - tokenUsage.promptTokens.write * writeRate + - tokenUsage.promptTokens.read * readRate; + tokenUsage.promptTokens.write * (writeRate ?? 0) + + tokenUsage.promptTokens.read * (readRate ?? 0); const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; - expect(result.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); - expect(result.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); + expect(result?.prompt?.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result?.completion?.completion).toBeCloseTo(-expectedCompletionCost, 0); }); it('should charge standard rates for structured tokens when below threshold', async () => { @@ -870,12 +901,12 @@ describe('spendTokens', () => { const expectedPromptCost = tokenUsage.promptTokens.input * standardPromptRate + - tokenUsage.promptTokens.write * writeRate + - tokenUsage.promptTokens.read * readRate; + tokenUsage.promptTokens.write * (writeRate ?? 0) + + tokenUsage.promptTokens.read * (readRate ?? 0); const expectedCompletionCost = tokenUsage.completionTokens * standardCompletionRate; - expect(result.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); - expect(result.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); + expect(result?.prompt?.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result?.completion?.completion).toBeCloseTo(-expectedCompletionCost, 0); }); it('should charge standard rates for gemini-3.1-pro-preview when prompt tokens are below threshold', async () => { @@ -1032,7 +1063,7 @@ describe('spendTokens', () => { promptTokens * tokenValues[model].prompt + completionTokens * tokenValues[model].completion; const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(balance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); }); @@ -1058,11 +1089,11 @@ describe('spendTokens', () => { const completionTx = transactions.find((t) => t.tokenType === 'completion'); const promptTx = transactions.find((t) => t.tokenType === 'prompt'); - expect(Math.abs(promptTx.rawAmount)).toBe(0); - expect(completionTx.rawAmount).toBe(-100); + expect(Math.abs(promptTx?.rawAmount ?? 0)).toBe(0); + expect(completionTx?.rawAmount).toBe(-100); const standardCompletionRate = tokenValues['claude-opus-4-6'].completion; - expect(completionTx.rate).toBe(standardCompletionRate); + expect(completionTx?.rate).toBe(standardCompletionRate); }); it('should use normalized inputTokenCount for premium threshold check on completion', async () => { @@ -1092,8 +1123,8 @@ describe('spendTokens', () => { const premiumPromptRate = premiumTokenValues[model].prompt; const premiumCompletionRate = premiumTokenValues[model].completion; - expect(promptTx.rate).toBe(premiumPromptRate); - expect(completionTx.rate).toBe(premiumCompletionRate); + expect(promptTx?.rate).toBe(premiumPromptRate); + expect(completionTx?.rate).toBe(premiumCompletionRate); }); it('should keep inputTokenCount as zero when promptTokens is zero', async () => { @@ -1116,10 +1147,10 @@ describe('spendTokens', () => { const completionTx = transactions.find((t) => t.tokenType === 'completion'); const promptTx = transactions.find((t) => t.tokenType === 'prompt'); - expect(Math.abs(promptTx.rawAmount)).toBe(0); + expect(Math.abs(promptTx?.rawAmount ?? 0)).toBe(0); const standardCompletionRate = tokenValues['claude-opus-4-6'].completion; - expect(completionTx.rate).toBe(standardCompletionRate); + expect(completionTx?.rate).toBe(standardCompletionRate); }); it('should not trigger premium pricing with negative promptTokens on premium model', async () => { @@ -1144,7 +1175,7 @@ describe('spendTokens', () => { const completionTx = transactions.find((t) => t.tokenType === 'completion'); const standardCompletionRate = tokenValues[model].completion; - expect(completionTx.rate).toBe(standardCompletionRate); + expect(completionTx?.rate).toBe(standardCompletionRate); }); it('should normalize negative structured token values to zero in spendStructuredTokens', async () => { @@ -1178,14 +1209,14 @@ describe('spendTokens', () => { const completionTx = transactions.find((t) => t.tokenType === 'completion'); const promptTx = transactions.find((t) => t.tokenType === 'prompt'); - expect(Math.abs(promptTx.inputTokens)).toBe(0); - expect(promptTx.writeTokens).toBe(-50); - expect(Math.abs(promptTx.readTokens)).toBe(0); + expect(Math.abs(promptTx?.inputTokens ?? 0)).toBe(0); + expect(promptTx?.writeTokens).toBe(-50); + expect(Math.abs(promptTx?.readTokens ?? 0)).toBe(0); - expect(Math.abs(completionTx.rawAmount)).toBe(0); + expect(Math.abs(completionTx?.rawAmount ?? 0)).toBe(0); const standardRate = tokenValues[model].completion; - expect(completionTx.rate).toBe(standardRate); + expect(completionTx?.rate).toBe(standardRate); }); }); }); diff --git a/packages/data-schemas/src/methods/spendTokens.ts b/packages/data-schemas/src/methods/spendTokens.ts new file mode 100644 index 0000000000..4cb6167b55 --- /dev/null +++ b/packages/data-schemas/src/methods/spendTokens.ts @@ -0,0 +1,145 @@ +import logger from '~/config/winston'; +import type { TxData, TransactionResult } from './transaction'; + +/** Base transaction context passed by callers — does not include fields added internally */ +export interface SpendTxData { + user: string | import('mongoose').Types.ObjectId; + conversationId?: string; + model?: string; + context?: string; + endpointTokenConfig?: Record> | null; + balance?: { enabled?: boolean }; + transactions?: { enabled?: boolean }; + valueKey?: string; +} + +export function createSpendTokensMethods( + _mongoose: typeof import('mongoose'), + transactionMethods: { + createTransaction: (txData: TxData) => Promise; + createStructuredTransaction: (txData: TxData) => Promise; + }, +) { + /** + * Creates up to two transactions to record the spending of tokens. + */ + async function spendTokens( + txData: SpendTxData, + tokenUsage: { promptTokens?: number; completionTokens?: number }, + ) { + const { promptTokens, completionTokens } = tokenUsage; + logger.debug( + `[spendTokens] conversationId: ${txData.conversationId}${ + txData?.context ? ` | Context: ${txData?.context}` : '' + } | Token usage: `, + { promptTokens, completionTokens }, + ); + let prompt: TransactionResult | undefined, completion: TransactionResult | undefined; + const normalizedPromptTokens = Math.max(promptTokens ?? 0, 0); + try { + if (promptTokens !== undefined) { + prompt = await transactionMethods.createTransaction({ + ...txData, + tokenType: 'prompt', + rawAmount: promptTokens === 0 ? 0 : -normalizedPromptTokens, + inputTokenCount: normalizedPromptTokens, + }); + } + + if (completionTokens !== undefined) { + completion = await transactionMethods.createTransaction({ + ...txData, + tokenType: 'completion', + rawAmount: completionTokens === 0 ? 0 : -Math.max(completionTokens, 0), + inputTokenCount: normalizedPromptTokens, + }); + } + + if (prompt || completion) { + logger.debug('[spendTokens] Transaction data record against balance:', { + user: txData.user, + prompt: prompt?.prompt, + promptRate: prompt?.rate, + completion: completion?.completion, + completionRate: completion?.rate, + balance: completion?.balance ?? prompt?.balance, + }); + } else { + logger.debug('[spendTokens] No transactions incurred against balance'); + } + } catch (err) { + logger.error('[spendTokens]', err); + } + } + + /** + * Creates transactions to record the spending of structured tokens. + */ + async function spendStructuredTokens( + txData: SpendTxData, + tokenUsage: { + promptTokens?: { input?: number; write?: number; read?: number }; + completionTokens?: number; + }, + ) { + const { promptTokens, completionTokens } = tokenUsage; + logger.debug( + `[spendStructuredTokens] conversationId: ${txData.conversationId}${ + txData?.context ? ` | Context: ${txData?.context}` : '' + } | Token usage: `, + { promptTokens, completionTokens }, + ); + let prompt: TransactionResult | undefined, completion: TransactionResult | undefined; + try { + if (promptTokens) { + const input = Math.max(promptTokens.input ?? 0, 0); + const write = Math.max(promptTokens.write ?? 0, 0); + const read = Math.max(promptTokens.read ?? 0, 0); + const totalInputTokens = input + write + read; + prompt = await transactionMethods.createStructuredTransaction({ + ...txData, + tokenType: 'prompt', + inputTokens: -input, + writeTokens: -write, + readTokens: -read, + inputTokenCount: totalInputTokens, + }); + } + + if (completionTokens) { + const totalInputTokens = promptTokens + ? Math.max(promptTokens.input ?? 0, 0) + + Math.max(promptTokens.write ?? 0, 0) + + Math.max(promptTokens.read ?? 0, 0) + : undefined; + completion = await transactionMethods.createTransaction({ + ...txData, + tokenType: 'completion', + rawAmount: -Math.max(completionTokens, 0), + inputTokenCount: totalInputTokens, + }); + } + + if (prompt || completion) { + logger.debug('[spendStructuredTokens] Transaction data record against balance:', { + user: txData.user, + prompt: prompt?.prompt, + promptRate: prompt?.rate, + completion: completion?.completion, + completionRate: completion?.rate, + balance: completion?.balance ?? prompt?.balance, + }); + } else { + logger.debug('[spendStructuredTokens] No transactions incurred against balance'); + } + } catch (err) { + logger.error('[spendStructuredTokens]', err); + } + + return { prompt, completion }; + } + + return { spendTokens, spendStructuredTokens }; +} + +export type SpendTokensMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/test-helpers.ts b/packages/data-schemas/src/methods/test-helpers.ts new file mode 100644 index 0000000000..26b5038dd6 --- /dev/null +++ b/packages/data-schemas/src/methods/test-helpers.ts @@ -0,0 +1,38 @@ +/** + * Inlined utility functions previously imported from @librechat/api. + * These are used only by test files in data-schemas. + */ + +/** + * Finds the first matching pattern in a tokens/values map by reverse-iterating + * and checking if the model name (lowercased) includes the key. + * + * Inlined from @librechat/api findMatchingPattern + */ +export function findMatchingPattern( + modelName: string, + tokensMap: Record, +): string | undefined { + const keys = Object.keys(tokensMap); + const lowerModelName = modelName.toLowerCase(); + for (let i = keys.length - 1; i >= 0; i--) { + const modelKey = keys[i]; + if (lowerModelName.includes(modelKey)) { + return modelKey; + } + } + return undefined; +} + +/** + * Matches a model name to a canonical key. When no maxTokensMap is available + * (as in data-schemas tests), returns the model name as-is. + * + * Inlined from @librechat/api matchModelName (simplified for test use) + */ +export function matchModelName(modelName: string, _endpoint?: string): string | undefined { + if (typeof modelName !== 'string') { + return undefined; + } + return modelName; +} diff --git a/packages/data-schemas/src/methods/toolCall.ts b/packages/data-schemas/src/methods/toolCall.ts new file mode 100644 index 0000000000..49dfb627e0 --- /dev/null +++ b/packages/data-schemas/src/methods/toolCall.ts @@ -0,0 +1,97 @@ +import type { Model } from 'mongoose'; + +interface IToolCallData { + messageId?: string; + conversationId?: string; + user?: string; + [key: string]: unknown; +} + +export function createToolCallMethods(mongoose: typeof import('mongoose')) { + /** + * Create a new tool call + */ + async function createToolCall(toolCallData: IToolCallData) { + try { + const ToolCall = mongoose.models.ToolCall as Model; + return await ToolCall.create(toolCallData); + } catch (error) { + throw new Error(`Error creating tool call: ${(error as Error).message}`); + } + } + + /** + * Get a tool call by ID + */ + async function getToolCallById(id: string) { + try { + const ToolCall = mongoose.models.ToolCall as Model; + return await ToolCall.findById(id).lean(); + } catch (error) { + throw new Error(`Error fetching tool call: ${(error as Error).message}`); + } + } + + /** + * Get tool calls by message ID and user + */ + async function getToolCallsByMessage(messageId: string, userId: string) { + try { + const ToolCall = mongoose.models.ToolCall as Model; + return await ToolCall.find({ messageId, user: userId }).lean(); + } catch (error) { + throw new Error(`Error fetching tool calls: ${(error as Error).message}`); + } + } + + /** + * Get tool calls by conversation ID and user + */ + async function getToolCallsByConvo(conversationId: string, userId: string) { + try { + const ToolCall = mongoose.models.ToolCall as Model; + return await ToolCall.find({ conversationId, user: userId }).lean(); + } catch (error) { + throw new Error(`Error fetching tool calls: ${(error as Error).message}`); + } + } + + /** + * Update a tool call + */ + async function updateToolCall(id: string, updateData: Partial) { + try { + const ToolCall = mongoose.models.ToolCall as Model; + return await ToolCall.findByIdAndUpdate(id, updateData, { new: true }).lean(); + } catch (error) { + throw new Error(`Error updating tool call: ${(error as Error).message}`); + } + } + + /** + * Delete tool calls by user and optionally conversation + */ + async function deleteToolCalls(userId: string, conversationId?: string) { + try { + const ToolCall = mongoose.models.ToolCall as Model; + const query: Record = { user: userId }; + if (conversationId) { + query.conversationId = conversationId; + } + return await ToolCall.deleteMany(query); + } catch (error) { + throw new Error(`Error deleting tool call: ${(error as Error).message}`); + } + } + + return { + createToolCall, + updateToolCall, + deleteToolCalls, + getToolCallById, + getToolCallsByConvo, + getToolCallsByMessage, + }; +} + +export type ToolCallMethods = ReturnType; diff --git a/api/models/Transaction.spec.js b/packages/data-schemas/src/methods/transaction.spec.ts similarity index 89% rename from api/models/Transaction.spec.js rename to packages/data-schemas/src/methods/transaction.spec.ts index f363c472e1..feaf9b758f 100644 --- a/api/models/Transaction.spec.js +++ b/packages/data-schemas/src/methods/transaction.spec.ts @@ -1,16 +1,63 @@ -const mongoose = require('mongoose'); -const { recordCollectedUsage } = require('@librechat/api'); -const { createMethods } = require('@librechat/data-schemas'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { getMultiplier, getCacheMultiplier, premiumTokenValues, tokenValues } = require('./tx'); -const { createTransaction, createStructuredTransaction } = require('./Transaction'); -const { spendTokens, spendStructuredTokens } = require('./spendTokens'); -const { Balance, Transaction } = require('~/db/models'); +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import type { ITransaction } from '~/schema/transaction'; +import type { TxData } from './transaction'; +import type { IBalance } from '..'; +import { createTxMethods, tokenValues, premiumTokenValues } from './tx'; +import { matchModelName, findMatchingPattern } from './test-helpers'; +import { createSpendTokensMethods } from './spendTokens'; +import { createTransactionMethods } from './transaction'; +import { createModels } from '~/models'; + +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +})); + +let mongoServer: InstanceType; +let Balance: mongoose.Model; +let Transaction: mongoose.Model; +let spendTokens: ReturnType['spendTokens']; +let spendStructuredTokens: ReturnType['spendStructuredTokens']; +let createTransaction: ReturnType['createTransaction']; +let createStructuredTransaction: ReturnType< + typeof createTransactionMethods +>['createStructuredTransaction']; +let getMultiplier: ReturnType['getMultiplier']; +let getCacheMultiplier: ReturnType['getCacheMultiplier']; -let mongoServer; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); + + // Register models + const models = createModels(mongoose); + Object.assign(mongoose.models, models); + + Balance = mongoose.models.Balance; + Transaction = mongoose.models.Transaction; + + // Create methods from factories (following the chain in methods/index.ts) + const txMethods = createTxMethods(mongoose, { matchModelName, findMatchingPattern }); + getMultiplier = txMethods.getMultiplier; + getCacheMultiplier = txMethods.getCacheMultiplier; + + const transactionMethods = createTransactionMethods(mongoose, { + getMultiplier: txMethods.getMultiplier, + getCacheMultiplier: txMethods.getCacheMultiplier, + }); + createTransaction = transactionMethods.createTransaction; + createStructuredTransaction = transactionMethods.createStructuredTransaction; + + const spendMethods = createSpendTokensMethods(mongoose, { + createTransaction: transactionMethods.createTransaction, + createStructuredTransaction: transactionMethods.createStructuredTransaction, + }); + spendTokens = spendMethods.spendTokens; + spendStructuredTokens = spendMethods.spendStructuredTokens; + await mongoose.connect(mongoUri); }); @@ -55,7 +102,7 @@ describe('Regular Token Spending Tests', () => { const expectedTotalCost = 100 * promptMultiplier + 50 * completionMultiplier; const expectedBalance = initialBalance - expectedTotalCost; - expect(updatedBalance.tokenCredits).toBeCloseTo(expectedBalance, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(expectedBalance, 0); }); test('spendTokens should handle zero completion tokens', async () => { @@ -86,7 +133,7 @@ describe('Regular Token Spending Tests', () => { const updatedBalance = await Balance.findOne({ user: userId }); const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); const expectedCost = 100 * promptMultiplier; - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendTokens should handle undefined token counts', async () => { @@ -139,7 +186,7 @@ describe('Regular Token Spending Tests', () => { const updatedBalance = await Balance.findOne({ user: userId }); const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); const expectedCost = 100 * promptMultiplier; - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendTokens should not update balance when balance feature is disabled', async () => { @@ -168,7 +215,7 @@ describe('Regular Token Spending Tests', () => { // Assert: Balance should remain unchanged. const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBe(initialBalance); + expect(updatedBalance?.tokenCredits).toBe(initialBalance); }); }); @@ -209,23 +256,25 @@ describe('Structured Token Spending Tests', () => { // Calculate expected costs. const expectedPromptCost = tokenUsage.promptTokens.input * promptMultiplier + - tokenUsage.promptTokens.write * writeMultiplier + - tokenUsage.promptTokens.read * readMultiplier; + tokenUsage.promptTokens.write * (writeMultiplier ?? 0) + + tokenUsage.promptTokens.read * (readMultiplier ?? 0); const expectedCompletionCost = tokenUsage.completionTokens * completionMultiplier; const expectedTotalCost = expectedPromptCost + expectedCompletionCost; const expectedBalance = initialBalance - expectedTotalCost; // Assert - expect(result.completion.balance).toBeLessThan(initialBalance); + expect(result?.completion?.balance).toBeLessThan(initialBalance); const allowedDifference = 100; - expect(Math.abs(result.completion.balance - expectedBalance)).toBeLessThan(allowedDifference); - const balanceDecrease = initialBalance - result.completion.balance; + expect(Math.abs((result?.completion?.balance ?? 0) - expectedBalance)).toBeLessThan( + allowedDifference, + ); + const balanceDecrease = initialBalance - (result?.completion?.balance ?? 0); expect(balanceDecrease).toBeCloseTo(expectedTotalCost, 0); const expectedPromptTokenValue = -expectedPromptCost; const expectedCompletionTokenValue = -expectedCompletionCost; - expect(result.prompt.prompt).toBeCloseTo(expectedPromptTokenValue, 1); - expect(result.completion.completion).toBe(expectedCompletionTokenValue); + expect(result?.prompt?.prompt).toBeCloseTo(expectedPromptTokenValue, 1); + expect(result?.completion?.completion).toBe(expectedCompletionTokenValue); }); test('should handle zero completion tokens in structured spending', async () => { @@ -258,7 +307,7 @@ describe('Structured Token Spending Tests', () => { // Assert expect(result.prompt).toBeDefined(); expect(result.completion).toBeUndefined(); - expect(result.prompt.prompt).toBeLessThan(0); + expect(result?.prompt?.prompt).toBeLessThan(0); }); test('should handle only prompt tokens in structured spending', async () => { @@ -290,7 +339,7 @@ describe('Structured Token Spending Tests', () => { // Assert expect(result.prompt).toBeDefined(); expect(result.completion).toBeUndefined(); - expect(result.prompt.prompt).toBeLessThan(0); + expect(result?.prompt?.prompt).toBeLessThan(0); }); test('should handle undefined token counts in structured spending', async () => { @@ -349,7 +398,7 @@ describe('Structured Token Spending Tests', () => { // Assert: // (Assuming a multiplier for completion of 15 and a cancel rate of 1.15 as noted in the original test.) - expect(result.completion.completion).toBeCloseTo(-50 * 15 * 1.15, 0); + expect(result?.completion?.completion).toBeCloseTo(-50 * 15 * 1.15, 0); }); }); @@ -361,7 +410,7 @@ describe('NaN Handling Tests', () => { await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; - const txData = { + const txData: TxData = { user: userId, conversationId: 'test-conversation-id', model, @@ -378,7 +427,7 @@ describe('NaN Handling Tests', () => { // Assert: No transaction should be created and balance remains unchanged. expect(result).toBeUndefined(); const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); + expect(balance?.tokenCredits).toBe(initialBalance); }); }); @@ -390,7 +439,7 @@ describe('Transactions Config Tests', () => { await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; - const txData = { + const txData: TxData = { user: userId, conversationId: 'test-conversation-id', model, @@ -409,7 +458,7 @@ describe('Transactions Config Tests', () => { const transactions = await Transaction.find({ user: userId }); expect(transactions).toHaveLength(0); const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); + expect(balance?.tokenCredits).toBe(initialBalance); }); test('createTransaction should save when transactions.enabled is true', async () => { @@ -419,7 +468,7 @@ describe('Transactions Config Tests', () => { await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; - const txData = { + const txData: TxData = { user: userId, conversationId: 'test-conversation-id', model, @@ -436,7 +485,7 @@ describe('Transactions Config Tests', () => { // Assert: Transaction should be created expect(result).toBeDefined(); - expect(result.balance).toBeLessThan(initialBalance); + expect(result?.balance).toBeLessThan(initialBalance); const transactions = await Transaction.find({ user: userId }); expect(transactions).toHaveLength(1); expect(transactions[0].rawAmount).toBe(-100); @@ -449,7 +498,7 @@ describe('Transactions Config Tests', () => { await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; - const txData = { + const txData: TxData = { user: userId, conversationId: 'test-conversation-id', model, @@ -466,7 +515,7 @@ describe('Transactions Config Tests', () => { // Assert: Transaction should be created (backward compatibility) expect(result).toBeDefined(); - expect(result.balance).toBeLessThan(initialBalance); + expect(result?.balance).toBeLessThan(initialBalance); const transactions = await Transaction.find({ user: userId }); expect(transactions).toHaveLength(1); }); @@ -478,7 +527,7 @@ describe('Transactions Config Tests', () => { await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; - const txData = { + const txData: TxData = { user: userId, conversationId: 'test-conversation-id', model, @@ -499,7 +548,7 @@ describe('Transactions Config Tests', () => { expect(transactions).toHaveLength(1); expect(transactions[0].rawAmount).toBe(-100); const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); + expect(balance?.tokenCredits).toBe(initialBalance); }); test('createStructuredTransaction should not save when transactions.enabled is false', async () => { @@ -509,7 +558,7 @@ describe('Transactions Config Tests', () => { await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'claude-3-5-sonnet'; - const txData = { + const txData: TxData = { user: userId, conversationId: 'test-conversation-id', model, @@ -529,7 +578,7 @@ describe('Transactions Config Tests', () => { const transactions = await Transaction.find({ user: userId }); expect(transactions).toHaveLength(0); const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); + expect(balance?.tokenCredits).toBe(initialBalance); }); test('createStructuredTransaction should save transaction but not update balance when balance is disabled but transactions enabled', async () => { @@ -539,7 +588,7 @@ describe('Transactions Config Tests', () => { await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'claude-3-5-sonnet'; - const txData = { + const txData: TxData = { user: userId, conversationId: 'test-conversation-id', model, @@ -563,7 +612,7 @@ describe('Transactions Config Tests', () => { expect(transactions[0].writeTokens).toBe(-100); expect(transactions[0].readTokens).toBe(-5); const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); + expect(balance?.tokenCredits).toBe(initialBalance); }); }); @@ -587,11 +636,11 @@ describe('calculateTokenValue Edge Cases', () => { }); const expectedRate = getMultiplier({ model, tokenType: 'prompt' }); - expect(result.rate).toBe(expectedRate); + expect(result?.rate).toBe(expectedRate); const tx = await Transaction.findOne({ user: userId }); - expect(tx.tokenValue).toBe(-promptTokens * expectedRate); - expect(tx.rate).toBe(expectedRate); + expect(tx?.tokenValue).toBe(-promptTokens * expectedRate); + expect(tx?.rate).toBe(expectedRate); }); test('should derive valueKey and apply correct rate for an unknown model with tokenType', async () => { @@ -610,9 +659,9 @@ describe('calculateTokenValue Edge Cases', () => { }); const tx = await Transaction.findOne({ user: userId }); - expect(tx.rate).toBeDefined(); - expect(tx.rate).toBeGreaterThan(0); - expect(tx.tokenValue).toBe(tx.rawAmount * tx.rate); + expect(tx?.rate).toBeDefined(); + expect(tx?.rate).toBeGreaterThan(0); + expect(tx?.tokenValue).toBe((tx?.rawAmount ?? 0) * (tx?.rate ?? 0)); }); test('should correctly apply model-derived multiplier without valueKey for completion', async () => { @@ -635,10 +684,10 @@ describe('calculateTokenValue Edge Cases', () => { const expectedRate = getMultiplier({ model, tokenType: 'completion' }); expect(expectedRate).toBe(tokenValues[model].completion); - expect(result.rate).toBe(expectedRate); + expect(result?.rate).toBe(expectedRate); const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo( + expect(updatedBalance?.tokenCredits).toBeCloseTo( initialBalance - completionTokens * expectedRate, 0, ); @@ -672,7 +721,7 @@ describe('Premium Token Pricing Integration Tests', () => { promptTokens * standardPromptRate + completionTokens * standardCompletionRate; const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendTokens should apply premium pricing when prompt tokens exceed premium threshold', async () => { @@ -701,7 +750,7 @@ describe('Premium Token Pricing Integration Tests', () => { promptTokens * premiumPromptRate + completionTokens * premiumCompletionRate; const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendTokens should apply standard pricing at exactly the premium threshold', async () => { @@ -730,7 +779,7 @@ describe('Premium Token Pricing Integration Tests', () => { promptTokens * standardPromptRate + completionTokens * standardCompletionRate; const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendStructuredTokens should apply premium pricing when total input tokens exceed threshold', async () => { @@ -769,14 +818,14 @@ describe('Premium Token Pricing Integration Tests', () => { const expectedPromptCost = tokenUsage.promptTokens.input * premiumPromptRate + - tokenUsage.promptTokens.write * writeMultiplier + - tokenUsage.promptTokens.read * readMultiplier; + tokenUsage.promptTokens.write * (writeMultiplier ?? 0) + + tokenUsage.promptTokens.read * (readMultiplier ?? 0); const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; const expectedTotalCost = expectedPromptCost + expectedCompletionCost; const updatedBalance = await Balance.findOne({ user: userId }); expect(totalInput).toBeGreaterThan(premiumTokenValues[model].threshold); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); }); test('spendStructuredTokens should apply standard pricing when total input tokens are below threshold', async () => { @@ -815,14 +864,14 @@ describe('Premium Token Pricing Integration Tests', () => { const expectedPromptCost = tokenUsage.promptTokens.input * standardPromptRate + - tokenUsage.promptTokens.write * writeMultiplier + - tokenUsage.promptTokens.read * readMultiplier; + tokenUsage.promptTokens.write * (writeMultiplier ?? 0) + + tokenUsage.promptTokens.read * (readMultiplier ?? 0); const expectedCompletionCost = tokenUsage.completionTokens * standardCompletionRate; const expectedTotalCost = expectedPromptCost + expectedCompletionCost; const updatedBalance = await Balance.findOne({ user: userId }); expect(totalInput).toBeLessThanOrEqual(premiumTokenValues[model].threshold); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); }); test('spendTokens should apply standard pricing for gemini-3.1-pro-preview below threshold', async () => { @@ -984,7 +1033,7 @@ describe('Premium Token Pricing Integration Tests', () => { promptTokens * standardPromptRate + completionTokens * standardCompletionRate; const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); }); diff --git a/packages/data-schemas/src/methods/transaction.ts b/packages/data-schemas/src/methods/transaction.ts index d521b9e85e..3f019defa2 100644 --- a/packages/data-schemas/src/methods/transaction.ts +++ b/packages/data-schemas/src/methods/transaction.ts @@ -1,24 +1,199 @@ -import type { IBalance, TransactionData } from '~/types'; import logger from '~/config/winston'; +import type { FilterQuery, Model, Types } from 'mongoose'; +import type { ITransaction } from '~/schema/transaction'; +import type { IBalance, IBalanceUpdate } from '~/types'; -interface UpdateBalanceParams { - user: string; - incrementValue: number; - setValues?: Partial>; +const cancelRate = 1.15; + +type MultiplierParams = { + model?: string; + valueKey?: string; + tokenType?: 'prompt' | 'completion'; + inputTokenCount?: number; + endpointTokenConfig?: Record>; +}; + +type CacheMultiplierParams = { + cacheType?: 'write' | 'read'; + model?: string; + endpointTokenConfig?: Record>; +}; + +/** Fields read/written by the internal token value calculators */ +interface InternalTxDoc { + valueKey?: string; + tokenType?: 'prompt' | 'completion' | 'credits'; + model?: string; + endpointTokenConfig?: Record> | null; + inputTokenCount?: number; + rawAmount?: number; + context?: string; + rate?: number; + tokenValue?: number; + rateDetail?: Record; + inputTokens?: number; + writeTokens?: number; + readTokens?: number; } -export function createTransactionMethods(mongoose: typeof import('mongoose')) { - async function updateBalance({ user, incrementValue, setValues }: UpdateBalanceParams) { +/** Input data for creating a transaction */ +export interface TxData { + user: string | Types.ObjectId; + conversationId?: string; + model?: string; + context?: string; + tokenType?: 'prompt' | 'completion' | 'credits'; + rawAmount?: number; + valueKey?: string; + endpointTokenConfig?: Record> | null; + inputTokenCount?: number; + inputTokens?: number; + writeTokens?: number; + readTokens?: number; + balance?: { enabled?: boolean }; + transactions?: { enabled?: boolean }; +} + +/** Return value from a successful transaction that also updates the balance */ +export interface TransactionResult { + rate: number; + user: string; + balance: number; + prompt?: number; + completion?: number; + credits?: number; +} + +export function createTransactionMethods( + mongoose: typeof import('mongoose'), + txMethods: { + getMultiplier: (params: MultiplierParams) => number; + getCacheMultiplier: (params: CacheMultiplierParams) => number | null; + }, +) { + /** Calculate and set the tokenValue for a transaction */ + function calculateTokenValue(txn: InternalTxDoc) { + const { valueKey, tokenType, model, endpointTokenConfig, inputTokenCount } = txn; + const multiplier = Math.abs( + txMethods.getMultiplier({ + valueKey, + tokenType: tokenType as 'prompt' | 'completion' | undefined, + model, + endpointTokenConfig: endpointTokenConfig ?? undefined, + inputTokenCount, + }), + ); + txn.rate = multiplier; + txn.tokenValue = (txn.rawAmount ?? 0) * multiplier; + if (txn.context && txn.tokenType === 'completion' && txn.context === 'incomplete') { + txn.tokenValue = Math.ceil((txn.tokenValue ?? 0) * cancelRate); + txn.rate = (txn.rate ?? 0) * cancelRate; + } + } + + /** Calculate token value for structured tokens */ + function calculateStructuredTokenValue(txn: InternalTxDoc) { + if (!txn.tokenType) { + txn.tokenValue = txn.rawAmount; + return; + } + + const { model, endpointTokenConfig, inputTokenCount } = txn; + const etConfig = endpointTokenConfig ?? undefined; + + if (txn.tokenType === 'prompt') { + const inputMultiplier = txMethods.getMultiplier({ + tokenType: 'prompt', + model, + endpointTokenConfig: etConfig, + inputTokenCount, + }); + const writeMultiplier = + txMethods.getCacheMultiplier({ + cacheType: 'write', + model, + endpointTokenConfig: etConfig, + }) ?? inputMultiplier; + const readMultiplier = + txMethods.getCacheMultiplier({ cacheType: 'read', model, endpointTokenConfig: etConfig }) ?? + inputMultiplier; + + txn.rateDetail = { + input: inputMultiplier, + write: writeMultiplier, + read: readMultiplier, + }; + + const totalPromptTokens = + Math.abs(txn.inputTokens ?? 0) + + Math.abs(txn.writeTokens ?? 0) + + Math.abs(txn.readTokens ?? 0); + + if (totalPromptTokens > 0) { + txn.rate = + (Math.abs(inputMultiplier * (txn.inputTokens ?? 0)) + + Math.abs(writeMultiplier * (txn.writeTokens ?? 0)) + + Math.abs(readMultiplier * (txn.readTokens ?? 0))) / + totalPromptTokens; + } else { + txn.rate = Math.abs(inputMultiplier); + } + + txn.tokenValue = -( + Math.abs(txn.inputTokens ?? 0) * inputMultiplier + + Math.abs(txn.writeTokens ?? 0) * writeMultiplier + + Math.abs(txn.readTokens ?? 0) * readMultiplier + ); + + txn.rawAmount = -totalPromptTokens; + } else if (txn.tokenType === 'completion') { + const multiplier = txMethods.getMultiplier({ + tokenType: txn.tokenType, + model, + endpointTokenConfig: etConfig, + inputTokenCount, + }); + txn.rate = Math.abs(multiplier); + txn.tokenValue = -Math.abs(txn.rawAmount ?? 0) * multiplier; + txn.rawAmount = -Math.abs(txn.rawAmount ?? 0); + } + + if (txn.context && txn.tokenType === 'completion' && txn.context === 'incomplete') { + txn.tokenValue = Math.ceil((txn.tokenValue ?? 0) * cancelRate); + txn.rate = (txn.rate ?? 0) * cancelRate; + if (txn.rateDetail) { + txn.rateDetail = Object.fromEntries( + Object.entries(txn.rateDetail).map(([k, v]) => [k, v * cancelRate]), + ); + } + } + } + + /** + * Updates a user's token balance using optimistic concurrency control. + * Always returns an IBalance or throws after exhausting retries. + */ + async function updateBalance({ + user, + incrementValue, + setValues, + }: { + user: string; + incrementValue: number; + setValues?: IBalanceUpdate; + }): Promise { + const Balance = mongoose.models.Balance as Model; const maxRetries = 10; let delay = 50; let lastError: Error | null = null; - const Balance = mongoose.models.Balance; for (let attempt = 1; attempt <= maxRetries; attempt++) { + let currentBalanceDoc; try { - const currentBalanceDoc = await Balance.findOne({ user }).lean(); - const currentCredits = currentBalanceDoc?.tokenCredits ?? 0; - const newCredits = Math.max(0, currentCredits + incrementValue); + currentBalanceDoc = await Balance.findOne({ user }).lean(); + const currentCredits = currentBalanceDoc ? currentBalanceDoc.tokenCredits : 0; + const potentialNewCredits = currentCredits + incrementValue; + const newCredits = Math.max(0, potentialNewCredits); const updatePayload = { $set: { @@ -27,8 +202,9 @@ export function createTransactionMethods(mongoose: typeof import('mongoose')) { }, }; + let updatedBalance: IBalance | null = null; if (currentBalanceDoc) { - const updatedBalance = await Balance.findOneAndUpdate( + updatedBalance = await Balance.findOneAndUpdate( { user, tokenCredits: currentCredits }, updatePayload, { new: true }, @@ -40,7 +216,7 @@ export function createTransactionMethods(mongoose: typeof import('mongoose')) { lastError = new Error(`Concurrency conflict for user ${user} on attempt ${attempt}.`); } else { try { - const updatedBalance = await Balance.findOneAndUpdate({ user }, updatePayload, { + updatedBalance = await Balance.findOneAndUpdate({ user }, updatePayload, { upsert: true, new: true, }).lean(); @@ -86,15 +262,162 @@ export function createTransactionMethods(mongoose: typeof import('mongoose')) { ); } - /** Bypasses document middleware; all computed fields must be pre-calculated before calling. */ - async function bulkInsertTransactions(docs: TransactionData[]): Promise { + /** + * Creates an auto-refill transaction that also updates balance. + */ + async function createAutoRefillTransaction(txData: TxData) { + if (txData.rawAmount != null && isNaN(txData.rawAmount)) { + return; + } const Transaction = mongoose.models.Transaction; - if (docs.length) { - await Transaction.insertMany(docs); + const transaction = new Transaction(txData); + transaction.endpointTokenConfig = txData.endpointTokenConfig; + transaction.inputTokenCount = txData.inputTokenCount; + calculateTokenValue(transaction); + await transaction.save(); + + const balanceResponse = await updateBalance({ + user: transaction.user as string, + incrementValue: txData.rawAmount ?? 0, + setValues: { lastRefill: new Date() }, + }); + const result = { + rate: transaction.rate as number, + user: transaction.user.toString() as string, + balance: balanceResponse.tokenCredits, + transaction, + }; + logger.debug('[Balance.check] Auto-refill performed', result); + return result; + } + + /** + * Creates a transaction and updates the balance. + */ + async function createTransaction(_txData: TxData): Promise { + const { balance, transactions, ...txData } = _txData; + if (txData.rawAmount != null && isNaN(txData.rawAmount)) { + return; + } + + if (transactions?.enabled === false) { + return; + } + + const Transaction = mongoose.models.Transaction; + const transaction = new Transaction(txData); + transaction.endpointTokenConfig = txData.endpointTokenConfig; + transaction.inputTokenCount = txData.inputTokenCount; + calculateTokenValue(transaction); + + await transaction.save(); + if (!balance?.enabled) { + return; + } + + const incrementValue = transaction.tokenValue as number; + const balanceResponse = await updateBalance({ + user: transaction.user as string, + incrementValue, + }); + + return { + rate: transaction.rate as number, + user: transaction.user.toString() as string, + balance: balanceResponse.tokenCredits, + [transaction.tokenType as string]: incrementValue, + } as TransactionResult; + } + + /** + * Creates a structured transaction and updates the balance. + */ + async function createStructuredTransaction( + _txData: TxData, + ): Promise { + const { balance, transactions, ...txData } = _txData; + if (transactions?.enabled === false) { + return; + } + + const Transaction = mongoose.models.Transaction; + const transaction = new Transaction(txData); + transaction.endpointTokenConfig = txData.endpointTokenConfig; + transaction.inputTokenCount = txData.inputTokenCount; + + calculateStructuredTokenValue(transaction); + + await transaction.save(); + + if (!balance?.enabled) { + return; + } + + const incrementValue = transaction.tokenValue as number; + + const balanceResponse = await updateBalance({ + user: transaction.user as string, + incrementValue, + }); + + return { + rate: transaction.rate as number, + user: transaction.user.toString() as string, + balance: balanceResponse.tokenCredits, + [transaction.tokenType as string]: incrementValue, + } as TransactionResult; + } + + /** + * Queries and retrieves transactions based on a given filter. + */ + async function getTransactions(filter: FilterQuery) { + try { + const Transaction = mongoose.models.Transaction; + return await Transaction.find(filter).lean(); + } catch (error) { + logger.error('Error querying transactions:', error); + throw error; } } - return { updateBalance, bulkInsertTransactions }; + /** Retrieves a user's balance record. */ + async function findBalanceByUser(user: string): Promise { + const Balance = mongoose.models.Balance as Model; + return Balance.findOne({ user }).lean(); + } + + /** Upserts balance fields for a user. */ + async function upsertBalanceFields( + user: string, + fields: IBalanceUpdate, + ): Promise { + const Balance = mongoose.models.Balance as Model; + return Balance.findOneAndUpdate({ user }, { $set: fields }, { upsert: true, new: true }).lean(); + } + + /** Deletes transactions matching a filter. */ + async function deleteTransactions(filter: FilterQuery) { + const Transaction = mongoose.models.Transaction; + return Transaction.deleteMany(filter); + } + + /** Deletes balance records matching a filter. */ + async function deleteBalances(filter: FilterQuery) { + const Balance = mongoose.models.Balance as Model; + return Balance.deleteMany(filter); + } + + return { + findBalanceByUser, + upsertBalanceFields, + getTransactions, + deleteTransactions, + deleteBalances, + createTransaction, + createAutoRefillTransaction, + createStructuredTransaction, + }; } export type TransactionMethods = ReturnType; diff --git a/api/models/tx.spec.js b/packages/data-schemas/src/methods/tx.spec.ts similarity index 95% rename from api/models/tx.spec.js rename to packages/data-schemas/src/methods/tx.spec.ts index 666cd0a3b8..d1e12e5a55 100644 --- a/api/models/tx.spec.js +++ b/packages/data-schemas/src/methods/tx.spec.ts @@ -1,16 +1,18 @@ /** Note: No hard-coded values should be used in this file. */ -const { maxTokensMap } = require('@librechat/api'); -const { EModelEndpoint } = require('librechat-data-provider'); -const { - defaultRate, +import { matchModelName, findMatchingPattern } from './test-helpers'; +import { EModelEndpoint } from 'librechat-data-provider'; +import { + createTxMethods, tokenValues, - getValueKey, - getMultiplier, - getPremiumRate, cacheTokenValues, - getCacheMultiplier, premiumTokenValues, -} = require('./tx'); + defaultRate, +} from './tx'; + +const { getValueKey, getMultiplier, getPremiumRate, getCacheMultiplier } = createTxMethods( + {} as typeof import('mongoose'), + { matchModelName, findMatchingPattern }, +); describe('getValueKey', () => { it('should return "16k" for model name containing "gpt-3.5-turbo-16k"', () => { @@ -263,6 +265,7 @@ describe('getMultiplier', () => { }); it('should return defaultRate if tokenType is provided but not found in tokenValues', () => { + // @ts-expect-error: intentionally passing invalid tokenType to test error handling expect(getMultiplier({ valueKey: '8k', tokenType: 'unknownType' })).toBe(defaultRate); }); @@ -606,7 +609,7 @@ describe('AWS Bedrock Model Tests', () => { const results = awsModels.map((model) => { const valueKey = getValueKey(model, EModelEndpoint.bedrock); const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' }); - return tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt; + return tokenValues[valueKey!].prompt && multiplier === tokenValues[valueKey!].prompt; }); expect(results.every(Boolean)).toBe(true); }); @@ -615,7 +618,7 @@ describe('AWS Bedrock Model Tests', () => { const results = awsModels.map((model) => { const valueKey = getValueKey(model, EModelEndpoint.bedrock); const multiplier = getMultiplier({ valueKey, tokenType: 'completion' }); - return tokenValues[valueKey].completion && multiplier === tokenValues[valueKey].completion; + return tokenValues[valueKey!].completion && multiplier === tokenValues[valueKey!].completion; }); expect(results.every(Boolean)).toBe(true); }); @@ -871,7 +874,7 @@ describe('Deepseek Model Tests', () => { const results = deepseekModels.map((model) => { const valueKey = getValueKey(model); const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' }); - return tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt; + return tokenValues[valueKey!].prompt && multiplier === tokenValues[valueKey!].prompt; }); expect(results.every(Boolean)).toBe(true); }); @@ -880,7 +883,7 @@ describe('Deepseek Model Tests', () => { const results = deepseekModels.map((model) => { const valueKey = getValueKey(model); const multiplier = getMultiplier({ valueKey, tokenType: 'completion' }); - return tokenValues[valueKey].completion && multiplier === tokenValues[valueKey].completion; + return tokenValues[valueKey!].completion && multiplier === tokenValues[valueKey!].completion; }); expect(results.every(Boolean)).toBe(true); }); @@ -890,7 +893,7 @@ describe('Deepseek Model Tests', () => { const valueKey = getValueKey(model); expect(valueKey).toBe(model); const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' }); - const result = tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt; + const result = tokenValues[valueKey!].prompt && multiplier === tokenValues[valueKey!].prompt; expect(result).toBe(true); }); @@ -1355,6 +1358,7 @@ describe('getCacheMultiplier', () => { it('should return null if cacheType is provided but not found in cacheTokenValues', () => { expect( + // @ts-expect-error: intentionally passing invalid cacheType to test error handling getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'unknownType' }), ).toBeNull(); }); @@ -1529,8 +1533,8 @@ describe('Google Model Tests', () => { }); results.forEach(({ valueKey, promptRate, completionRate }) => { - expect(promptRate).toBe(tokenValues[valueKey].prompt); - expect(completionRate).toBe(tokenValues[valueKey].completion); + expect(promptRate).toBe(tokenValues[valueKey!].prompt); + expect(completionRate).toBe(tokenValues[valueKey!].completion); }); }); @@ -2310,7 +2314,7 @@ describe('Premium Token Pricing', () => { it('should return null from getPremiumRate when inputTokenCount is undefined or null', () => { expect(getPremiumRate(premiumModel, 'prompt', undefined)).toBeNull(); - expect(getPremiumRate(premiumModel, 'prompt', null)).toBeNull(); + expect(getPremiumRate(premiumModel, 'prompt', undefined)).toBeNull(); }); it('should return null from getPremiumRate for models without premium pricing', () => { @@ -2412,118 +2416,5 @@ describe('Premium Token Pricing', () => { }); }); -describe('tokens.ts and tx.js sync validation', () => { - it('should resolve all models in maxTokensMap to pricing via getValueKey', () => { - const tokensKeys = Object.keys(maxTokensMap[EModelEndpoint.openAI]); - const txKeys = Object.keys(tokenValues); - - const unresolved = []; - - tokensKeys.forEach((key) => { - // Skip legacy token size mappings (e.g., '4k', '8k', '16k', '32k') - if (/^\d+k$/.test(key)) return; - - // Skip generic pattern keys (end with '-' or ':') - if (key.endsWith('-') || key.endsWith(':')) return; - - // Try to resolve via getValueKey - const resolvedKey = getValueKey(key); - - // If it resolves and the resolved key has pricing, success - if (resolvedKey && txKeys.includes(resolvedKey)) return; - - // If it resolves to a legacy key (4k, 8k, etc), also OK - if (resolvedKey && /^\d+k$/.test(resolvedKey)) return; - - // If we get here, this model can't get pricing - flag it - unresolved.push({ - key, - resolvedKey: resolvedKey || 'undefined', - context: maxTokensMap[EModelEndpoint.openAI][key], - }); - }); - - if (unresolved.length > 0) { - console.log('\nModels that cannot resolve to pricing via getValueKey:'); - unresolved.forEach(({ key, resolvedKey, context }) => { - console.log(` - '${key}' → '${resolvedKey}' (context: ${context})`); - }); - } - - expect(unresolved).toEqual([]); - }); - - it('should not have redundant dated variants with same pricing and context as base model', () => { - const txKeys = Object.keys(tokenValues); - const redundant = []; - - txKeys.forEach((key) => { - // Check if this is a dated variant (ends with -YYYY-MM-DD) - if (key.match(/.*-\d{4}-\d{2}-\d{2}$/)) { - const baseKey = key.replace(/-\d{4}-\d{2}-\d{2}$/, ''); - - if (txKeys.includes(baseKey)) { - const variantPricing = tokenValues[key]; - const basePricing = tokenValues[baseKey]; - const variantContext = maxTokensMap[EModelEndpoint.openAI][key]; - const baseContext = maxTokensMap[EModelEndpoint.openAI][baseKey]; - - const samePricing = - variantPricing.prompt === basePricing.prompt && - variantPricing.completion === basePricing.completion; - const sameContext = variantContext === baseContext; - - if (samePricing && sameContext) { - redundant.push({ - key, - baseKey, - pricing: `${variantPricing.prompt}/${variantPricing.completion}`, - context: variantContext, - }); - } - } - } - }); - - if (redundant.length > 0) { - console.log('\nRedundant dated variants found (same pricing and context as base):'); - redundant.forEach(({ key, baseKey, pricing, context }) => { - console.log(` - '${key}' → '${baseKey}' (pricing: ${pricing}, context: ${context})`); - console.log(` Can be removed - pattern matching will handle it`); - }); - } - - expect(redundant).toEqual([]); - }); - - it('should have context windows in tokens.ts for all models with pricing in tx.js (openAI catch-all)', () => { - const txKeys = Object.keys(tokenValues); - const missingContext = []; - - txKeys.forEach((key) => { - // Skip legacy token size mappings (4k, 8k, 16k, 32k) - if (/^\d+k$/.test(key)) return; - - // Check if this model has a context window defined - const context = maxTokensMap[EModelEndpoint.openAI][key]; - - if (!context) { - const pricing = tokenValues[key]; - missingContext.push({ - key, - pricing: `${pricing.prompt}/${pricing.completion}`, - }); - } - }); - - if (missingContext.length > 0) { - console.log('\nModels with pricing but missing context in tokens.ts:'); - missingContext.forEach(({ key, pricing }) => { - console.log(` - '${key}' (pricing: ${pricing})`); - console.log(` Add to tokens.ts openAIModels/bedrockModels/etc.`); - }); - } - - expect(missingContext).toEqual([]); - }); -}); +// Cross-package sync validation tests (tokens.ts ↔ tx.ts) moved to +// packages/api tests since they require maxTokensMap from @librechat/api. diff --git a/api/models/tx.js b/packages/data-schemas/src/methods/tx.ts similarity index 63% rename from api/models/tx.js rename to packages/data-schemas/src/methods/tx.ts index ce14fad3a0..23b22353dc 100644 --- a/api/models/tx.js +++ b/packages/data-schemas/src/methods/tx.ts @@ -1,44 +1,43 @@ -const { matchModelName, findMatchingPattern } = require('@librechat/api'); -const defaultRate = 6; - /** * Token Pricing Configuration * * Pattern Matching * ================ - * `findMatchingPattern` (from @librechat/api) uses `modelName.includes(key)` and selects - * the LONGEST matching key. If a key's length equals the model name's length (exact match), - * it returns immediately. Definition order does NOT affect correctness. + * `findMatchingPattern` uses `modelName.includes(key)` and selects the **longest** + * matching key. If a key's length equals the model name's length (exact match), it + * returns immediately — no further keys are checked. * - * Key ordering matters only for: - * 1. Performance: list older/less common models first so newer/common models - * are found earlier in the reverse scan. - * 2. Same-length tie-breaking: the last-defined key wins on equal-length matches. - * - * This applies to BOTH `tokenValues` and `cacheTokenValues` objects. + * For keys of different lengths, definition order does not affect the result — the + * longest match always wins. For **same-length ties**, the function iterates in + * reverse, so the last-defined key wins. Key ordering therefore matters for: + * 1. **Performance**: list older/legacy models first, newer models last — newer + * models are more commonly used and will match earlier in the reverse scan. + * 2. **Same-length tie-breaking**: when two keys of equal length both match, + * the last-defined key wins. */ -/** - * AWS Bedrock pricing - * source: https://aws.amazon.com/bedrock/pricing/ - */ -const bedrockValues = { - // Basic llama2 patterns (base defaults to smallest variant) +export interface TxDeps { + /** From @librechat/api — matches a model name to a canonical key. */ + matchModelName: (model: string, endpoint?: string) => string | undefined; + /** From @librechat/api — finds the longest key in `values` whose key is a substring of `model`. */ + findMatchingPattern: (model: string, values: Record) => string | undefined; +} + +export const defaultRate = 6; + +/** AWS Bedrock pricing (source: https://aws.amazon.com/bedrock/pricing/) */ +const bedrockValues: Record = { llama2: { prompt: 0.75, completion: 1.0 }, 'llama-2': { prompt: 0.75, completion: 1.0 }, 'llama2-13b': { prompt: 0.75, completion: 1.0 }, 'llama2:70b': { prompt: 1.95, completion: 2.56 }, 'llama2-70b': { prompt: 1.95, completion: 2.56 }, - - // Basic llama3 patterns (base defaults to smallest variant) llama3: { prompt: 0.3, completion: 0.6 }, 'llama-3': { prompt: 0.3, completion: 0.6 }, 'llama3-8b': { prompt: 0.3, completion: 0.6 }, 'llama3:8b': { prompt: 0.3, completion: 0.6 }, 'llama3-70b': { prompt: 2.65, completion: 3.5 }, 'llama3:70b': { prompt: 2.65, completion: 3.5 }, - - // llama3-x-Nb pattern (base defaults to smallest variant) 'llama3-1': { prompt: 0.22, completion: 0.22 }, 'llama3-1-8b': { prompt: 0.22, completion: 0.22 }, 'llama3-1-70b': { prompt: 0.72, completion: 0.72 }, @@ -50,8 +49,6 @@ const bedrockValues = { 'llama3-2-90b': { prompt: 0.72, completion: 0.72 }, 'llama3-3': { prompt: 2.65, completion: 3.5 }, 'llama3-3-70b': { prompt: 2.65, completion: 3.5 }, - - // llama3.x:Nb pattern (base defaults to smallest variant) 'llama3.1': { prompt: 0.22, completion: 0.22 }, 'llama3.1:8b': { prompt: 0.22, completion: 0.22 }, 'llama3.1:70b': { prompt: 0.72, completion: 0.72 }, @@ -63,8 +60,6 @@ const bedrockValues = { 'llama3.2:90b': { prompt: 0.72, completion: 0.72 }, 'llama3.3': { prompt: 2.65, completion: 3.5 }, 'llama3.3:70b': { prompt: 2.65, completion: 3.5 }, - - // llama-3.x-Nb pattern (base defaults to smallest variant) 'llama-3.1': { prompt: 0.22, completion: 0.22 }, 'llama-3.1-8b': { prompt: 0.22, completion: 0.22 }, 'llama-3.1-70b': { prompt: 0.72, completion: 0.72 }, @@ -83,21 +78,17 @@ const bedrockValues = { 'mistral-large-2407': { prompt: 3.0, completion: 9.0 }, 'command-text': { prompt: 1.5, completion: 2.0 }, 'command-light': { prompt: 0.3, completion: 0.6 }, - // AI21 models 'j2-mid': { prompt: 12.5, completion: 12.5 }, 'j2-ultra': { prompt: 18.8, completion: 18.8 }, 'jamba-instruct': { prompt: 0.5, completion: 0.7 }, - // Amazon Titan models 'titan-text-lite': { prompt: 0.15, completion: 0.2 }, 'titan-text-express': { prompt: 0.2, completion: 0.6 }, 'titan-text-premier': { prompt: 0.5, completion: 1.5 }, - // Amazon Nova models 'nova-micro': { prompt: 0.035, completion: 0.14 }, 'nova-lite': { prompt: 0.06, completion: 0.24 }, 'nova-pro': { prompt: 0.8, completion: 3.2 }, 'nova-premier': { prompt: 2.5, completion: 12.5 }, 'deepseek.r1': { prompt: 1.35, completion: 5.4 }, - // Moonshot/Kimi models on Bedrock 'moonshot.kimi': { prompt: 0.6, completion: 2.5 }, 'moonshot.kimi-k2': { prompt: 0.6, completion: 2.5 }, 'moonshot.kimi-k2.5': { prompt: 0.6, completion: 3.0 }, @@ -107,23 +98,19 @@ const bedrockValues = { /** * Mapping of model token sizes to their respective multipliers for prompt and completion. * The rates are 1 USD per 1M tokens. - * @type {Object.} */ -const tokenValues = Object.assign( +export const tokenValues: Record = Object.assign( { - // Legacy token size mappings (generic patterns - check LAST) '8k': { prompt: 30, completion: 60 }, '32k': { prompt: 60, completion: 120 }, '4k': { prompt: 1.5, completion: 2 }, '16k': { prompt: 3, completion: 4 }, - // Generic fallback patterns (check LAST) 'claude-': { prompt: 0.8, completion: 2.4 }, deepseek: { prompt: 0.28, completion: 0.42 }, command: { prompt: 0.38, completion: 0.38 }, - gemma: { prompt: 0.02, completion: 0.04 }, // Base pattern (using gemma-3n-e4b pricing) + gemma: { prompt: 0.02, completion: 0.04 }, gemini: { prompt: 0.5, completion: 1.5 }, 'gpt-oss': { prompt: 0.05, completion: 0.2 }, - // Specific model variants (check FIRST - more specific patterns at end) 'gpt-3.5-turbo-1106': { prompt: 1, completion: 2 }, 'gpt-3.5-turbo-0125': { prompt: 0.5, completion: 1.5 }, 'gpt-4-1106': { prompt: 10, completion: 30 }, @@ -176,16 +163,16 @@ const tokenValues = Object.assign( 'deepseek-reasoner': { prompt: 0.28, completion: 0.42 }, 'deepseek-r1': { prompt: 0.4, completion: 2.0 }, 'deepseek-v3': { prompt: 0.2, completion: 0.8 }, - 'gemma-2': { prompt: 0.01, completion: 0.03 }, // Base pattern (using gemma-2-9b pricing) - 'gemma-3': { prompt: 0.02, completion: 0.04 }, // Base pattern (using gemma-3n-e4b pricing) + 'gemma-2': { prompt: 0.01, completion: 0.03 }, + 'gemma-3': { prompt: 0.02, completion: 0.04 }, 'gemma-3-27b': { prompt: 0.09, completion: 0.16 }, 'gemini-1.5': { prompt: 2.5, completion: 10 }, 'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 }, 'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 }, - 'gemini-2.0': { prompt: 0.1, completion: 0.4 }, // Base pattern (using 2.0-flash pricing) + 'gemini-2.0': { prompt: 0.1, completion: 0.4 }, 'gemini-2.0-flash': { prompt: 0.1, completion: 0.4 }, 'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 }, - 'gemini-2.5': { prompt: 0.3, completion: 2.5 }, // Base pattern (using 2.5-flash pricing) + 'gemini-2.5': { prompt: 0.3, completion: 2.5 }, 'gemini-2.5-flash': { prompt: 0.3, completion: 2.5 }, 'gemini-2.5-flash-lite': { prompt: 0.1, completion: 0.4 }, 'gemini-2.5-pro': { prompt: 1.25, completion: 10 }, @@ -195,7 +182,7 @@ const tokenValues = Object.assign( 'gemini-3.1': { prompt: 2, completion: 12 }, 'gemini-3.1-flash-lite': { prompt: 0.25, completion: 1.5 }, 'gemini-pro-vision': { prompt: 0.5, completion: 1.5 }, - grok: { prompt: 2.0, completion: 10.0 }, // Base pattern defaults to grok-2 + grok: { prompt: 2.0, completion: 10.0 }, 'grok-beta': { prompt: 5.0, completion: 15.0 }, 'grok-vision-beta': { prompt: 5.0, completion: 15.0 }, 'grok-2': { prompt: 2.0, completion: 10.0 }, @@ -210,7 +197,7 @@ const tokenValues = Object.assign( 'grok-3-mini-fast': { prompt: 0.6, completion: 4 }, 'grok-4': { prompt: 3.0, completion: 15.0 }, 'grok-4-fast': { prompt: 0.2, completion: 0.5 }, - 'grok-4-1-fast': { prompt: 0.2, completion: 0.5 }, // covers reasoning & non-reasoning variants + 'grok-4-1-fast': { prompt: 0.2, completion: 0.5 }, 'grok-code-fast': { prompt: 0.2, completion: 1.5 }, codestral: { prompt: 0.3, completion: 0.9 }, 'ministral-3b': { prompt: 0.04, completion: 0.04 }, @@ -220,10 +207,9 @@ const tokenValues = Object.assign( 'pixtral-large': { prompt: 2.0, completion: 6.0 }, 'mistral-large': { prompt: 2.0, completion: 6.0 }, 'mixtral-8x22b': { prompt: 0.65, completion: 0.65 }, - // Moonshot/Kimi models (base patterns first, specific patterns last for correct matching) - kimi: { prompt: 0.6, completion: 2.5 }, // Base pattern - moonshot: { prompt: 2.0, completion: 5.0 }, // Base pattern (using 128k pricing) - 'kimi-latest': { prompt: 0.2, completion: 2.0 }, // Uses 8k/32k/128k pricing dynamically + kimi: { prompt: 0.6, completion: 2.5 }, + moonshot: { prompt: 2.0, completion: 5.0 }, + 'kimi-latest': { prompt: 0.2, completion: 2.0 }, 'kimi-k2': { prompt: 0.6, completion: 2.5 }, 'kimi-k2.5': { prompt: 0.6, completion: 3.0 }, 'kimi-k2-turbo': { prompt: 1.15, completion: 8.0 }, @@ -245,12 +231,10 @@ const tokenValues = Object.assign( 'moonshot-v1-128k': { prompt: 2.0, completion: 5.0 }, 'moonshot-v1-128k-vision': { prompt: 2.0, completion: 5.0 }, 'moonshot-v1-128k-vision-preview': { prompt: 2.0, completion: 5.0 }, - // GPT-OSS models (specific sizes) 'gpt-oss:20b': { prompt: 0.05, completion: 0.2 }, 'gpt-oss-20b': { prompt: 0.05, completion: 0.2 }, 'gpt-oss:120b': { prompt: 0.15, completion: 0.6 }, 'gpt-oss-120b': { prompt: 0.15, completion: 0.6 }, - // GLM models (Zhipu AI) - general to specific glm4: { prompt: 0.1, completion: 0.1 }, 'glm-4': { prompt: 0.1, completion: 0.1 }, 'glm-4-32b': { prompt: 0.1, completion: 0.1 }, @@ -258,26 +242,22 @@ const tokenValues = Object.assign( 'glm-4.5-air': { prompt: 0.14, completion: 0.86 }, 'glm-4.5v': { prompt: 0.6, completion: 1.8 }, 'glm-4.6': { prompt: 0.5, completion: 1.75 }, - // Qwen models - qwen: { prompt: 0.08, completion: 0.33 }, // Qwen base pattern (using qwen2.5-72b pricing) - 'qwen2.5': { prompt: 0.08, completion: 0.33 }, // Qwen 2.5 base pattern + qwen: { prompt: 0.08, completion: 0.33 }, + 'qwen2.5': { prompt: 0.08, completion: 0.33 }, 'qwen-turbo': { prompt: 0.05, completion: 0.2 }, 'qwen-plus': { prompt: 0.4, completion: 1.2 }, 'qwen-max': { prompt: 1.6, completion: 6.4 }, 'qwq-32b': { prompt: 0.15, completion: 0.4 }, - // Qwen3 models - qwen3: { prompt: 0.035, completion: 0.138 }, // Qwen3 base pattern (using qwen3-4b pricing) + qwen3: { prompt: 0.035, completion: 0.138 }, 'qwen3-8b': { prompt: 0.035, completion: 0.138 }, 'qwen3-14b': { prompt: 0.05, completion: 0.22 }, 'qwen3-30b-a3b': { prompt: 0.06, completion: 0.22 }, 'qwen3-32b': { prompt: 0.05, completion: 0.2 }, 'qwen3-235b-a22b': { prompt: 0.08, completion: 0.55 }, - // Qwen3 VL (Vision-Language) models 'qwen3-vl-8b-thinking': { prompt: 0.18, completion: 2.1 }, 'qwen3-vl-8b-instruct': { prompt: 0.18, completion: 0.69 }, 'qwen3-vl-30b-a3b': { prompt: 0.29, completion: 1.0 }, 'qwen3-vl-235b-a22b': { prompt: 0.3, completion: 1.2 }, - // Qwen3 specialized models 'qwen3-max': { prompt: 1.2, completion: 6 }, 'qwen3-coder': { prompt: 0.22, completion: 0.95 }, 'qwen3-coder-30b-a3b': { prompt: 0.06, completion: 0.25 }, @@ -290,11 +270,9 @@ const tokenValues = Object.assign( /** * Mapping of model token sizes to their respective multipliers for cached input, read and write. - * See Anthropic's documentation on this: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#pricing * The rates are 1 USD per 1M tokens. - * @type {Object.} */ -const cacheTokenValues = { +export const cacheTokenValues: Record = { 'claude-3.7-sonnet': { write: 3.75, read: 0.3 }, 'claude-3-7-sonnet': { write: 3.75, read: 0.3 }, 'claude-3.5-sonnet': { write: 3.75, read: 0.3 }, @@ -308,11 +286,6 @@ const cacheTokenValues = { 'claude-opus-4': { write: 18.75, read: 1.5 }, 'claude-opus-4-5': { write: 6.25, read: 0.5 }, 'claude-opus-4-6': { write: 6.25, read: 0.5 }, - // OpenAI models — cached input discount varies by family: - // gpt-4o (incl. mini), o1 (incl. mini/preview): 50% off - // gpt-4.1 (incl. mini/nano), o3 (incl. mini), o4-mini: 75% off - // gpt-5.x (excl. pro variants): 90% off - // gpt-5-pro, gpt-5.2-pro, gpt-5.4-pro: no caching 'gpt-4o': { write: 2.5, read: 1.25 }, 'gpt-4o-mini': { write: 0.15, read: 0.075 }, 'gpt-4.1': { write: 2, read: 0.5 }, @@ -331,11 +304,9 @@ const cacheTokenValues = { o3: { write: 2, read: 0.5 }, 'o3-mini': { write: 1.1, read: 0.275 }, 'o4-mini': { write: 1.1, read: 0.275 }, - // DeepSeek models - cache hit: $0.028/1M, cache miss: $0.28/1M deepseek: { write: 0.28, read: 0.028 }, 'deepseek-chat': { write: 0.28, read: 0.028 }, 'deepseek-reasoner': { write: 0.28, read: 0.028 }, - // Moonshot/Kimi models - cache hit: $0.15/1M (k2) or $0.10/1M (k2.5), cache miss: $0.60/1M kimi: { write: 0.6, read: 0.15 }, 'kimi-k2': { write: 0.6, read: 0.15 }, 'kimi-k2.5': { write: 0.6, read: 0.1 }, @@ -355,171 +326,169 @@ const cacheTokenValues = { /** * Premium (tiered) pricing for models whose rates change based on prompt size. - * Each entry specifies the token threshold and the rates that apply above it. - * @type {Object.} */ -const premiumTokenValues = { +export const premiumTokenValues: Record< + string, + { threshold: number; prompt: number; completion: number } +> = { 'claude-opus-4-6': { threshold: 200000, prompt: 10, completion: 37.5 }, 'claude-sonnet-4-6': { threshold: 200000, prompt: 6, completion: 22.5 }, 'gemini-3.1': { threshold: 200000, prompt: 4, completion: 18 }, }; -/** - * Retrieves the key associated with a given model name. - * - * @param {string} model - The model name to match. - * @param {string} endpoint - The endpoint name to match. - * @returns {string|undefined} The key corresponding to the model name, or undefined if no match is found. - */ -const getValueKey = (model, endpoint) => { - if (!model || typeof model !== 'string') { - return undefined; - } +export function createTxMethods(_mongoose: typeof import('mongoose'), txDeps: TxDeps) { + const { matchModelName, findMatchingPattern } = txDeps; - // Use findMatchingPattern directly against tokenValues for efficient lookup - if (!endpoint || (typeof endpoint === 'string' && !tokenValues[endpoint])) { - const matchedKey = findMatchingPattern(model, tokenValues); - if (matchedKey) { - return matchedKey; + /** + * Retrieves the key associated with a given model name. + */ + function getValueKey(model: string, endpoint?: string): string | undefined { + if (!model || typeof model !== 'string') { + return undefined; + } + + if (!endpoint || (typeof endpoint === 'string' && !tokenValues[endpoint])) { + const matchedKey = findMatchingPattern(model, tokenValues); + if (matchedKey) { + return matchedKey; + } + } + + const modelName = matchModelName(model, endpoint); + if (!modelName) { + return undefined; + } + + if (modelName.includes('gpt-3.5-turbo-16k')) { + return '16k'; + } else if (modelName.includes('gpt-3.5')) { + return '4k'; + } else if (modelName.includes('gpt-4-vision')) { + return 'gpt-4-1106'; + } else if (modelName.includes('gpt-4-0125')) { + return 'gpt-4-1106'; + } else if (modelName.includes('gpt-4-turbo')) { + return 'gpt-4-1106'; + } else if (modelName.includes('gpt-4-32k')) { + return '32k'; + } else if (modelName.includes('gpt-4')) { + return '8k'; } - } - // Fallback: use matchModelName for edge cases and legacy handling - const modelName = matchModelName(model, endpoint); - if (!modelName) { return undefined; } - // Legacy token size mappings and aliases for older models - if (modelName.includes('gpt-3.5-turbo-16k')) { - return '16k'; - } else if (modelName.includes('gpt-3.5')) { - return '4k'; - } else if (modelName.includes('gpt-4-vision')) { - return 'gpt-4-1106'; // Alias for gpt-4-vision - } else if (modelName.includes('gpt-4-0125')) { - return 'gpt-4-1106'; // Alias for gpt-4-0125 - } else if (modelName.includes('gpt-4-turbo')) { - return 'gpt-4-1106'; // Alias for gpt-4-turbo - } else if (modelName.includes('gpt-4-32k')) { - return '32k'; - } else if (modelName.includes('gpt-4')) { - return '8k'; + /** + * Checks if premium (tiered) pricing applies and returns the premium rate. + */ + function getPremiumRate( + valueKey: string, + tokenType: string, + inputTokenCount?: number, + ): number | null { + if (inputTokenCount == null) { + return null; + } + const premiumEntry = premiumTokenValues[valueKey]; + if (!premiumEntry || inputTokenCount <= premiumEntry.threshold) { + return null; + } + return premiumEntry[tokenType as 'prompt' | 'completion'] ?? null; } - return undefined; -}; + /** + * Retrieves the multiplier for a given value key and token type. + */ + function getMultiplier({ + model, + valueKey, + endpoint, + tokenType, + inputTokenCount, + endpointTokenConfig, + }: { + model?: string; + valueKey?: string; + endpoint?: string; + tokenType?: 'prompt' | 'completion'; + inputTokenCount?: number; + endpointTokenConfig?: Record>; + }): number { + if (endpointTokenConfig && model) { + return endpointTokenConfig?.[model]?.[tokenType as string] ?? defaultRate; + } -/** - * Retrieves the multiplier for a given value key and token type. If no value key is provided, - * it attempts to derive it from the model name. - * - * @param {Object} params - The parameters for the function. - * @param {string} [params.valueKey] - The key corresponding to the model name. - * @param {'prompt' | 'completion'} [params.tokenType] - The type of token (e.g., 'prompt' or 'completion'). - * @param {string} [params.model] - The model name to derive the value key from if not provided. - * @param {string} [params.endpoint] - The endpoint name to derive the value key from if not provided. - * @param {EndpointTokenConfig} [params.endpointTokenConfig] - The token configuration for the endpoint. - * @param {number} [params.inputTokenCount] - Total input token count for tiered pricing. - * @returns {number} The multiplier for the given parameters, or a default value if not found. - */ -const getMultiplier = ({ - model, - valueKey, - endpoint, - tokenType, - inputTokenCount, - endpointTokenConfig, -}) => { - if (endpointTokenConfig) { - return endpointTokenConfig?.[model]?.[tokenType] ?? defaultRate; - } + if (valueKey && tokenType) { + const premiumRate = getPremiumRate(valueKey, tokenType, inputTokenCount); + if (premiumRate != null) { + return premiumRate; + } + return tokenValues[valueKey]?.[tokenType] ?? defaultRate; + } + + if (!tokenType || !model) { + return 1; + } + + valueKey = getValueKey(model, endpoint); + if (!valueKey) { + return defaultRate; + } - if (valueKey && tokenType) { const premiumRate = getPremiumRate(valueKey, tokenType, inputTokenCount); if (premiumRate != null) { return premiumRate; } + return tokenValues[valueKey]?.[tokenType] ?? defaultRate; } - if (!tokenType || !model) { - return 1; - } + /** + * Retrieves the cache multiplier for a given value key and token type. + */ + function getCacheMultiplier({ + valueKey, + cacheType, + model, + endpoint, + endpointTokenConfig, + }: { + valueKey?: string; + cacheType?: 'write' | 'read'; + model?: string; + endpoint?: string; + endpointTokenConfig?: Record>; + }): number | null { + if (endpointTokenConfig && model) { + return endpointTokenConfig?.[model]?.[cacheType as string] ?? null; + } - valueKey = getValueKey(model, endpoint); - if (!valueKey) { - return defaultRate; - } + if (valueKey && cacheType) { + return cacheTokenValues[valueKey]?.[cacheType] ?? null; + } - const premiumRate = getPremiumRate(valueKey, tokenType, inputTokenCount); - if (premiumRate != null) { - return premiumRate; - } + if (!cacheType || !model) { + return null; + } - return tokenValues[valueKey]?.[tokenType] ?? defaultRate; -}; + valueKey = getValueKey(model, endpoint); + if (!valueKey) { + return null; + } -/** - * Checks if premium (tiered) pricing applies and returns the premium rate. - * Each model defines its own threshold in `premiumTokenValues`. - * @param {string} valueKey - * @param {string} tokenType - * @param {number} [inputTokenCount] - * @returns {number|null} - */ -const getPremiumRate = (valueKey, tokenType, inputTokenCount) => { - if (inputTokenCount == null) { - return null; - } - const premiumEntry = premiumTokenValues[valueKey]; - if (!premiumEntry || inputTokenCount <= premiumEntry.threshold) { - return null; - } - return premiumEntry[tokenType] ?? null; -}; - -/** - * Retrieves the cache multiplier for a given value key and token type. If no value key is provided, - * it attempts to derive it from the model name. - * - * @param {Object} params - The parameters for the function. - * @param {string} [params.valueKey] - The key corresponding to the model name. - * @param {'write' | 'read'} [params.cacheType] - The type of token (e.g., 'write' or 'read'). - * @param {string} [params.model] - The model name to derive the value key from if not provided. - * @param {string} [params.endpoint] - The endpoint name to derive the value key from if not provided. - * @param {EndpointTokenConfig} [params.endpointTokenConfig] - The token configuration for the endpoint. - * @returns {number | null} The multiplier for the given parameters, or `null` if not found. - */ -const getCacheMultiplier = ({ valueKey, cacheType, model, endpoint, endpointTokenConfig }) => { - if (endpointTokenConfig) { - return endpointTokenConfig?.[model]?.[cacheType] ?? null; - } - - if (valueKey && cacheType) { return cacheTokenValues[valueKey]?.[cacheType] ?? null; } - if (!cacheType || !model) { - return null; - } + return { + tokenValues, + premiumTokenValues, + getValueKey, + getMultiplier, + getPremiumRate, + getCacheMultiplier, + defaultRate, + cacheTokenValues, + }; +} - valueKey = getValueKey(model, endpoint); - if (!valueKey) { - return null; - } - - // If we got this far, and values[cacheType] is undefined somehow, return a rough average of default multipliers - return cacheTokenValues[valueKey]?.[cacheType] ?? null; -}; - -module.exports = { - tokenValues, - premiumTokenValues, - getValueKey, - getMultiplier, - getPremiumRate, - getCacheMultiplier, - defaultRate, - cacheTokenValues, -}; +export type TxMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/userGroup.ts b/packages/data-schemas/src/methods/userGroup.ts index f6b57095dc..5c683268b3 100644 --- a/packages/data-schemas/src/methods/userGroup.ts +++ b/packages/data-schemas/src/methods/userGroup.ts @@ -589,6 +589,61 @@ export function createUserGroupMethods(mongoose: typeof import('mongoose')) { return combined; } + /** + * Removes a user from all groups they belong to. + * @param userId - The user ID (or ObjectId) of the member to remove + */ + async function removeUserFromAllGroups(userId: string | Types.ObjectId): Promise { + const Group = mongoose.models.Group as Model; + await Group.updateMany({ memberIds: userId }, { $pullAll: { memberIds: [userId] } }); + } + + /** + * Finds a single group matching the given filter. + * @param filter - MongoDB filter query + */ + async function findGroupByQuery( + filter: Record, + session?: ClientSession, + ): Promise { + const Group = mongoose.models.Group as Model; + const query = Group.findOne(filter); + if (session) { + query.session(session); + } + return query.lean(); + } + + /** + * Updates a group by its ID. + * @param groupId - The group's ObjectId + * @param data - Fields to set via $set + */ + async function updateGroupById( + groupId: string | Types.ObjectId, + data: Record, + session?: ClientSession, + ): Promise { + const Group = mongoose.models.Group as Model; + const options = { new: true, ...(session ? { session } : {}) }; + return Group.findByIdAndUpdate(groupId, { $set: data }, options).lean(); + } + + /** + * Bulk-updates groups matching a filter. + * @param filter - MongoDB filter query + * @param update - Update operations + * @param options - Optional query options (e.g., { session }) + */ + async function bulkUpdateGroups( + filter: Record, + update: Record, + options?: { session?: ClientSession }, + ) { + const Group = mongoose.models.Group as Model; + return Group.updateMany(filter, update, options || {}); + } + return { findGroupById, findGroupByExternalId, @@ -598,6 +653,10 @@ export function createUserGroupMethods(mongoose: typeof import('mongoose')) { upsertGroupByExternalId, addUserToGroup, removeUserFromGroup, + removeUserFromAllGroups, + findGroupByQuery, + updateGroupById, + bulkUpdateGroups, getUserGroups, getUserPrincipals, syncUserEntraGroups, diff --git a/packages/data-schemas/src/types/agent.ts b/packages/data-schemas/src/types/agent.ts index f163ab63bd..1171028c5d 100644 --- a/packages/data-schemas/src/types/agent.ts +++ b/packages/data-schemas/src/types/agent.ts @@ -1,5 +1,5 @@ import { Document, Types } from 'mongoose'; -import type { GraphEdge, AgentToolOptions } from 'librechat-data-provider'; +import type { GraphEdge, AgentToolOptions, AgentToolResources } from 'librechat-data-provider'; export interface ISupportContact { name?: string; @@ -32,7 +32,7 @@ export interface IAgent extends Omit { agent_ids?: string[]; edges?: GraphEdge[]; conversation_starters?: string[]; - tool_resources?: unknown; + tool_resources?: AgentToolResources; versions?: Omit[]; category: string; support_contact?: ISupportContact; diff --git a/packages/data-schemas/src/types/balance.ts b/packages/data-schemas/src/types/balance.ts index d9497ff514..e5eb4c4f15 100644 --- a/packages/data-schemas/src/types/balance.ts +++ b/packages/data-schemas/src/types/balance.ts @@ -10,3 +10,14 @@ export interface IBalance extends Document { lastRefill: Date; refillAmount: number; } + +/** Plain data fields for creating or updating a balance record (no Mongoose Document methods) */ +export interface IBalanceUpdate { + user?: string; + tokenCredits?: number; + autoRefillEnabled?: boolean; + refillIntervalValue?: number; + refillIntervalUnit?: string; + refillAmount?: number; + lastRefill?: Date; +} diff --git a/packages/data-schemas/src/types/message.ts b/packages/data-schemas/src/types/message.ts index 2ca262a6bb..c4e96b34ba 100644 --- a/packages/data-schemas/src/types/message.ts +++ b/packages/data-schemas/src/types/message.ts @@ -11,7 +11,7 @@ export interface IMessage extends Document { conversationSignature?: string; clientId?: string; invocationId?: number; - parentMessageId?: string; + parentMessageId?: string | null; tokenCount?: number; summaryTokenCount?: number; sender?: string; @@ -40,7 +40,7 @@ export interface IMessage extends Document { addedConvo?: boolean; metadata?: Record; attachments?: unknown[]; - expiredAt?: Date; + expiredAt?: Date | null; createdAt?: Date; updatedAt?: Date; } diff --git a/packages/data-schemas/src/utils/index.ts b/packages/data-schemas/src/utils/index.ts index af47bf8855..626233f1be 100644 --- a/packages/data-schemas/src/utils/index.ts +++ b/packages/data-schemas/src/utils/index.ts @@ -1 +1,3 @@ +export * from './string'; +export * from './tempChatRetention'; export * from './transactions'; diff --git a/packages/data-schemas/src/utils/string.ts b/packages/data-schemas/src/utils/string.ts new file mode 100644 index 0000000000..6b92811b09 --- /dev/null +++ b/packages/data-schemas/src/utils/string.ts @@ -0,0 +1,6 @@ +/** + * Escapes special regex characters in a string. + */ +export function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/packages/api/src/utils/tempChatRetention.spec.ts b/packages/data-schemas/src/utils/tempChatRetention.spec.ts similarity index 98% rename from packages/api/src/utils/tempChatRetention.spec.ts rename to packages/data-schemas/src/utils/tempChatRetention.spec.ts index ef029cdde5..847088ab7c 100644 --- a/packages/api/src/utils/tempChatRetention.spec.ts +++ b/packages/data-schemas/src/utils/tempChatRetention.spec.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from '@librechat/data-schemas'; +import type { AppConfig } from '~/types'; import { createTempChatExpirationDate, getTempChatRetentionHours, diff --git a/packages/api/src/utils/tempChatRetention.ts b/packages/data-schemas/src/utils/tempChatRetention.ts similarity index 95% rename from packages/api/src/utils/tempChatRetention.ts rename to packages/data-schemas/src/utils/tempChatRetention.ts index eaa6ad2029..663228c13e 100644 --- a/packages/api/src/utils/tempChatRetention.ts +++ b/packages/data-schemas/src/utils/tempChatRetention.ts @@ -1,5 +1,5 @@ -import { logger } from '@librechat/data-schemas'; -import type { AppConfig } from '@librechat/data-schemas'; +import logger from '~/config/winston'; +import type { AppConfig } from '~/types'; /** * Default retention period for temporary chats in hours diff --git a/packages/data-schemas/tsconfig.build.json b/packages/data-schemas/tsconfig.build.json new file mode 100644 index 0000000000..79e86005cc --- /dev/null +++ b/packages/data-schemas/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationDir": "dist/types", + "outDir": "dist" + }, + "exclude": ["node_modules", "dist", "**/*.spec.ts"] +} diff --git a/packages/data-schemas/tsconfig.json b/packages/data-schemas/tsconfig.json index 57a321c866..b9829ce4e7 100644 --- a/packages/data-schemas/tsconfig.json +++ b/packages/data-schemas/tsconfig.json @@ -3,9 +3,8 @@ "target": "ES2019", "module": "ESNext", "moduleResolution": "node", - "declaration": true, - "declarationDir": "dist/types", - "outDir": "dist", + "declaration": false, + "noEmit": true, "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, @@ -19,5 +18,5 @@ } }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] + "exclude": ["node_modules", "dist"] } From 0412f05daf10d6e7d1c9ac9ddde3eaa0a0667fbb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 5 Mar 2026 16:01:52 -0500 Subject: [PATCH 096/111] =?UTF-8?q?=F0=9F=AA=A2=20chore:=20Consolidate=20P?= =?UTF-8?q?ricing=20and=20Tx=20Imports=20After=20tx.js=20Module=20Removal?= =?UTF-8?q?=20(#12086)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🧹 chore: resolve imports due to rebase * chore: Update model mocks in unit tests for consistency - Consolidated model mock implementations across various test files to streamline setup and reduce redundancy. - Removed duplicate mock definitions for `getMultiplier` and `getCacheMultiplier`, ensuring a unified approach in `recordCollectedUsage.spec.js`, `openai.spec.js`, `responses.unit.spec.js`, and `abortMiddleware.spec.js`. - Enhanced clarity and maintainability of test files by aligning mock structures with the latest model updates. * fix: Safeguard token credit checks in transaction tests - Updated assertions in `transaction.spec.ts` to handle potential null values for `updatedBalance` by using optional chaining. - Enhanced robustness of tests related to token credit calculations, ensuring they correctly account for scenarios where the balance may not be found. * chore: transaction methods with bulk insert functionality - Introduced `bulkInsertTransactions` method in `transaction.ts` to facilitate batch insertion of transaction documents. - Updated test file `transactions.bulk-parity.spec.ts` to utilize new pricing function assignments and handle potential null values in calculations, improving test robustness. - Refactored pricing function initialization for clarity and consistency. * refactor: Enhance type definitions and introduce new utility functions for model matching - Added `findMatchingPattern` and `matchModelName` utility functions to improve model name matching logic in transaction methods. - Updated type definitions for `findMatchingPattern` to accept a more specific tokensMap structure, enhancing type safety. - Refactored `dbMethods` initialization in `transactions.bulk-parity.spec.ts` to include the new utility functions, improving test clarity and functionality. * refactor: Update database method imports and enhance transaction handling - Refactored `abortMiddleware.js` to utilize centralized database methods for message handling and conversation retrieval, improving code consistency. - Enhanced `bulkInsertTransactions` in `transaction.ts` to handle empty document arrays gracefully and added error logging for better debugging. - Updated type definitions in `transactions.ts` to enforce stricter typing for token types, enhancing type safety across transaction methods. - Improved test setup in `transactions.bulk-parity.spec.ts` by refining pricing function assignments and ensuring robust handling of potential null values. * refactor: Update database method references and improve transaction multiplier handling - Refactored `client.js` to update database method references for `bulkInsertTransactions` and `updateBalance`, ensuring consistency in method usage. - Enhanced transaction multiplier calculations in `transaction.spec.ts` to provide fallback values for write and read multipliers, improving robustness in cost calculations across structured token spending tests. --- .../agents/__tests__/openai.spec.js | 7 +- .../agents/__tests__/responses.unit.spec.js | 7 +- api/server/controllers/agents/client.js | 6 +- api/server/controllers/agents/openai.js | 3 +- .../agents/recordCollectedUsage.spec.js | 6 - api/server/controllers/agents/responses.js | 5 +- api/server/middleware/abortMiddleware.js | 18 +- api/server/middleware/abortMiddleware.spec.js | 6 +- api/server/services/Files/process.spec.js | 7 +- .../agents/transactions.bulk-parity.spec.ts | 51 ++- packages/api/src/agents/transactions.ts | 8 +- packages/api/src/types/tokens.ts | 10 +- packages/data-schemas/src/methods/index.ts | 5 +- .../data-schemas/src/methods/test-helpers.ts | 2 +- .../src/methods/transaction.spec.ts | 387 ++---------------- .../data-schemas/src/methods/transaction.ts | 17 +- packages/data-schemas/src/methods/tx.ts | 5 +- 17 files changed, 123 insertions(+), 427 deletions(-) diff --git a/api/server/controllers/agents/__tests__/openai.spec.js b/api/server/controllers/agents/__tests__/openai.spec.js index cc43387560..deeb2ec51d 100644 --- a/api/server/controllers/agents/__tests__/openai.spec.js +++ b/api/server/controllers/agents/__tests__/openai.spec.js @@ -79,11 +79,6 @@ jest.mock('~/server/services/ToolService', () => ({ const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); -jest.mock('~/models/tx', () => ({ - getMultiplier: mockGetMultiplier, - getCacheMultiplier: mockGetCacheMultiplier, -})); - jest.mock('~/server/controllers/agents/callbacks', () => ({ createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), @@ -110,6 +105,8 @@ jest.mock('~/models', () => ({ bulkInsertTransactions: mockBulkInsertTransactions, spendTokens: mockSpendTokens, spendStructuredTokens: mockSpendStructuredTokens, + getMultiplier: mockGetMultiplier, + getCacheMultiplier: mockGetCacheMultiplier, getConvoFiles: jest.fn().mockResolvedValue([]), })); diff --git a/api/server/controllers/agents/__tests__/responses.unit.spec.js b/api/server/controllers/agents/__tests__/responses.unit.spec.js index 604c28f74d..0a63445f24 100644 --- a/api/server/controllers/agents/__tests__/responses.unit.spec.js +++ b/api/server/controllers/agents/__tests__/responses.unit.spec.js @@ -103,11 +103,6 @@ jest.mock('~/server/services/ToolService', () => ({ const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); -jest.mock('~/models/tx', () => ({ - getMultiplier: mockGetMultiplier, - getCacheMultiplier: mockGetCacheMultiplier, -})); - jest.mock('~/server/controllers/agents/callbacks', () => ({ createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), @@ -136,6 +131,8 @@ jest.mock('~/models', () => ({ bulkInsertTransactions: mockBulkInsertTransactions, spendTokens: mockSpendTokens, spendStructuredTokens: mockSpendStructuredTokens, + getMultiplier: mockGetMultiplier, + getCacheMultiplier: mockGetCacheMultiplier, getConvoFiles: jest.fn().mockResolvedValue([]), saveConvo: jest.fn().mockResolvedValue({}), getConvo: jest.fn().mockResolvedValue(null), diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 1724e20ada..bf75838a87 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -47,8 +47,6 @@ const { } = require('librechat-data-provider'); const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); const { encodeAndFormat } = require('~/server/services/Files/images/encode'); -const { updateBalance, bulkInsertTransactions } = require('~/models'); -const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); const { createContextHandlers } = require('~/app/clients/prompts'); const { getMCPServerTools } = require('~/server/services/Config'); const BaseClient = require('~/app/clients/BaseClient'); @@ -633,8 +631,8 @@ class AgentClient extends BaseClient { { spendTokens: db.spendTokens, spendStructuredTokens: db.spendStructuredTokens, - pricing: { getMultiplier, getCacheMultiplier }, - bulkWriteOps: { insertMany: bulkInsertTransactions, updateBalance }, + pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier }, + bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, { user: this.user ?? this.options.req.user?.id, diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index f1b199dede..ae2e462103 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -24,7 +24,6 @@ const { const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); const { createToolEndCallback } = require('~/server/controllers/agents/callbacks'); const { findAccessibleResources } = require('~/server/services/PermissionService'); -const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); const db = require('~/models'); /** @@ -510,7 +509,7 @@ const OpenAIChatCompletionController = async (req, res) => { { spendTokens: db.spendTokens, spendStructuredTokens: db.spendStructuredTokens, - pricing: { getMultiplier, getCacheMultiplier }, + pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, { diff --git a/api/server/controllers/agents/recordCollectedUsage.spec.js b/api/server/controllers/agents/recordCollectedUsage.spec.js index 2d4730c603..009c5b262c 100644 --- a/api/server/controllers/agents/recordCollectedUsage.spec.js +++ b/api/server/controllers/agents/recordCollectedUsage.spec.js @@ -21,14 +21,8 @@ const mockRecordCollectedUsage = jest jest.mock('~/models', () => ({ spendTokens: (...args) => mockSpendTokens(...args), spendStructuredTokens: (...args) => mockSpendStructuredTokens(...args), -})); - -jest.mock('~/models/tx', () => ({ getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier, -})); - -jest.mock('~/models', () => ({ updateBalance: mockUpdateBalance, bulkInsertTransactions: mockBulkInsertTransactions, })); diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index de185f4c2b..62cedb14fd 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -36,7 +36,6 @@ const { } = require('~/server/controllers/agents/callbacks'); const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); const { findAccessibleResources } = require('~/server/services/PermissionService'); -const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); const db = require('~/models'); /** @type {import('@librechat/api').AppConfig | null} */ @@ -528,7 +527,7 @@ const createResponse = async (req, res) => { { spendTokens: db.spendTokens, spendStructuredTokens: db.spendStructuredTokens, - pricing: { getMultiplier, getCacheMultiplier }, + pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, { @@ -683,7 +682,7 @@ const createResponse = async (req, res) => { { spendTokens: db.spendTokens, spendStructuredTokens: db.spendStructuredTokens, - pricing: { getMultiplier, getCacheMultiplier }, + pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, { diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index 624ace7f9f..e0c5ae0ff0 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -8,13 +8,11 @@ const { recordCollectedUsage, sanitizeMessageForTransmit, } = require('@librechat/api'); -const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider'); -const { saveMessage, getConvo, updateBalance, bulkInsertTransactions } = require('~/models'); const { truncateText, smartTruncateText } = require('~/app/clients/prompts'); -const { getMultiplier, getCacheMultiplier } = require('~/models/tx'); const clearPendingReq = require('~/cache/clearPendingReq'); const { sendError } = require('~/server/middleware/error'); const { abortRun } = require('./abortRun'); +const db = require('~/models'); /** * Spend tokens for all models from collected usage. @@ -44,10 +42,10 @@ async function spendCollectedUsage({ await recordCollectedUsage( { - spendTokens, - spendStructuredTokens, - pricing: { getMultiplier, getCacheMultiplier }, - bulkWriteOps: { insertMany: bulkInsertTransactions, updateBalance }, + spendTokens: db.spendTokens, + spendStructuredTokens: db.spendStructuredTokens, + pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier }, + bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, { user: userId, @@ -123,13 +121,13 @@ async function abortMessage(req, res) { }); } else { // Fallback: no collected usage, use text-based token counting for primary model only - await spendTokens( + await db.spendTokens( { ...responseMessage, context: 'incomplete', user: userId }, { promptTokens, completionTokens }, ); } - await saveMessage( + await db.saveMessage( { userId: req?.user?.id, isTemporary: req?.body?.isTemporary, @@ -140,7 +138,7 @@ async function abortMessage(req, res) { ); // Get conversation for title - const conversation = await getConvo(userId, conversationId); + const conversation = await db.getConvo(userId, conversationId); const finalEvent = { title: conversation && !conversation.title ? null : conversation?.title || 'New Chat', diff --git a/api/server/middleware/abortMiddleware.spec.js b/api/server/middleware/abortMiddleware.spec.js index c9c0d5cc60..a4ce85674b 100644 --- a/api/server/middleware/abortMiddleware.spec.js +++ b/api/server/middleware/abortMiddleware.spec.js @@ -20,8 +20,6 @@ const mockRecordCollectedUsage = jest const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); - - jest.mock('@librechat/data-schemas', () => ({ logger: { debug: jest.fn(), @@ -65,6 +63,10 @@ jest.mock('~/models', () => ({ getConvo: jest.fn().mockResolvedValue({ title: 'Test Chat' }), updateBalance: mockUpdateBalance, bulkInsertTransactions: mockBulkInsertTransactions, + spendTokens: (...args) => mockSpendTokens(...args), + spendStructuredTokens: (...args) => mockSpendStructuredTokens(...args), + getMultiplier: mockGetMultiplier, + getCacheMultiplier: mockGetCacheMultiplier, })); jest.mock('./abortRun', () => ({ diff --git a/api/server/services/Files/process.spec.js b/api/server/services/Files/process.spec.js index 7737255a52..39300161a8 100644 --- a/api/server/services/Files/process.spec.js +++ b/api/server/services/Files/process.spec.js @@ -30,11 +30,6 @@ jest.mock('~/server/controllers/assistants/v2', () => ({ deleteResourceFileId: jest.fn(), })); -jest.mock('~/models/Agent', () => ({ - addAgentResourceFile: jest.fn().mockResolvedValue({}), - removeAgentResourceFiles: jest.fn(), -})); - jest.mock('~/server/controllers/assistants/helpers', () => ({ getOpenAIClient: jest.fn(), })); @@ -47,6 +42,8 @@ jest.mock('~/models', () => ({ createFile: jest.fn().mockResolvedValue({ file_id: 'created-file-id' }), updateFileUsage: jest.fn(), deleteFiles: jest.fn(), + addAgentResourceFile: jest.fn().mockResolvedValue({}), + removeAgentResourceFiles: jest.fn(), })); jest.mock('~/server/utils/getFileStrategy', () => ({ diff --git a/packages/api/src/agents/transactions.bulk-parity.spec.ts b/packages/api/src/agents/transactions.bulk-parity.spec.ts index bf89682d6f..327856d18b 100644 --- a/packages/api/src/agents/transactions.bulk-parity.spec.ts +++ b/packages/api/src/agents/transactions.bulk-parity.spec.ts @@ -14,10 +14,12 @@ import mongoose from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; import { + tokenValues, CANCEL_RATE, createMethods, balanceSchema, transactionSchema, + premiumTokenValues, } from '@librechat/data-schemas'; import type { PricingFns, TxMetadata } from './transactions'; import { @@ -26,6 +28,26 @@ import { prepareTokenSpend, } from './transactions'; +/** Inlined from packages/data-schemas/src/methods/test-helpers.ts — keep in sync */ +function findMatchingPattern( + modelName: string, + tokensMap: Record>, +): string | undefined { + const keys = Object.keys(tokensMap); + const lowerModelName = modelName.toLowerCase(); + for (let i = keys.length - 1; i >= 0; i--) { + if (lowerModelName.includes(keys[i])) { + return keys[i]; + } + } + return undefined; +} + +/** Inlined from packages/data-schemas/src/methods/test-helpers.ts — keep in sync */ +function matchModelName(modelName: string, _endpoint?: string): string | undefined { + return typeof modelName === 'string' ? modelName : undefined; +} + jest.mock('@librechat/data-schemas', () => { const actual = jest.requireActual('@librechat/data-schemas'); return { @@ -34,29 +56,23 @@ jest.mock('@librechat/data-schemas', () => { }; }); -// Real pricing functions from api/models/tx.js — same ones the legacy path uses -/* eslint-disable @typescript-eslint/no-require-imports */ -const { - getMultiplier, - getCacheMultiplier, - tokenValues, - premiumTokenValues, -} = require('../../../../api/models/tx.js'); -/* eslint-enable @typescript-eslint/no-require-imports */ - -const pricing: PricingFns = { getMultiplier, getCacheMultiplier }; - let mongoServer: MongoMemoryServer; let Transaction: mongoose.Model; let Balance: mongoose.Model; let dbMethods: ReturnType; +let pricing: PricingFns; +let getMultiplier: ReturnType['getMultiplier']; +let getCacheMultiplier: ReturnType['getCacheMultiplier']; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); await mongoose.connect(mongoServer.getUri()); Transaction = mongoose.models.Transaction || mongoose.model('Transaction', transactionSchema); Balance = mongoose.models.Balance || mongoose.model('Balance', balanceSchema); - dbMethods = createMethods(mongoose); + dbMethods = createMethods(mongoose, { matchModelName, findMatchingPattern }); + getMultiplier = dbMethods.getMultiplier; + getCacheMultiplier = dbMethods.getCacheMultiplier; + pricing = { getMultiplier, getCacheMultiplier }; }); afterAll(async () => { @@ -536,8 +552,13 @@ describe('Multi-entry batch parity', () => { const premiumCompletionRate = (premiumTokenValues as Record>)[ model ].completion; - const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); - const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: totalInput, + }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; const expectedPromptCost = tokenUsage.promptTokens.input * premiumPromptRate + diff --git a/packages/api/src/agents/transactions.ts b/packages/api/src/agents/transactions.ts index b746392b44..a9eeda1973 100644 --- a/packages/api/src/agents/transactions.ts +++ b/packages/api/src/agents/transactions.ts @@ -3,9 +3,11 @@ import type { TCustomConfig, TTransactionsConfig } from 'librechat-data-provider import type { TransactionData } from '@librechat/data-schemas'; import type { EndpointTokenConfig } from '~/types/tokens'; +type TokenType = 'prompt' | 'completion'; + interface GetMultiplierParams { valueKey?: string; - tokenType?: string; + tokenType?: TokenType; model?: string; endpointTokenConfig?: EndpointTokenConfig; inputTokenCount?: number; @@ -34,14 +36,14 @@ interface BaseTxData { } interface StandardTxData extends BaseTxData { - tokenType: string; + tokenType: TokenType; rawAmount: number; inputTokenCount?: number; valueKey?: string; } interface StructuredTxData extends BaseTxData { - tokenType: string; + tokenType: TokenType; inputTokens?: number; writeTokens?: number; readTokens?: number; diff --git a/packages/api/src/types/tokens.ts b/packages/api/src/types/tokens.ts index f6e03d2e8d..b555031049 100644 --- a/packages/api/src/types/tokens.ts +++ b/packages/api/src/types/tokens.ts @@ -1,16 +1,8 @@ -/** Configuration object mapping model keys to their respective prompt, completion rates, and context limit - * - * Note: the [key: string]: unknown is not in the original JSDoc typedef in /api/typedefs.js, but I've included it since - * getModelMaxOutputTokens calls getModelTokenValue with a key of 'output', which was not in the original JSDoc typedef, - * but would be referenced in a TokenConfig in the if(matchedPattern) portion of getModelTokenValue. - * So in order to preserve functionality for that case and any others which might reference an additional key I'm unaware of, - * I've included it here until the interface can be typed more tightly. - */ export interface TokenConfig { + [key: string]: number; prompt: number; completion: number; context: number; - [key: string]: unknown; } /** An endpoint's config object mapping model keys to their respective prompt, completion rates, and context limit */ diff --git a/packages/data-schemas/src/methods/index.ts b/packages/data-schemas/src/methods/index.ts index 6246d74343..4192314b0b 100644 --- a/packages/data-schemas/src/methods/index.ts +++ b/packages/data-schemas/src/methods/index.ts @@ -85,7 +85,10 @@ export interface CreateMethodsDeps { /** Matches a model name to a canonical key. From @librechat/api. */ matchModelName?: (model: string, endpoint?: string) => string | undefined; /** Finds the first key in values whose key is a substring of model. From @librechat/api. */ - findMatchingPattern?: (model: string, values: Record) => string | undefined; + findMatchingPattern?: ( + model: string, + values: Record>, + ) => string | undefined; /** Removes all ACL permissions for a resource. From PermissionService. */ removeAllPermissions?: (params: { resourceType: string; resourceId: unknown }) => Promise; /** Returns a cache store for the given key. From getLogStores. */ diff --git a/packages/data-schemas/src/methods/test-helpers.ts b/packages/data-schemas/src/methods/test-helpers.ts index 26b5038dd6..bd64e0268a 100644 --- a/packages/data-schemas/src/methods/test-helpers.ts +++ b/packages/data-schemas/src/methods/test-helpers.ts @@ -11,7 +11,7 @@ */ export function findMatchingPattern( modelName: string, - tokensMap: Record, + tokensMap: Record>, ): string | undefined { const keys = Object.keys(tokensMap); const lowerModelName = modelName.toLowerCase(); diff --git a/packages/data-schemas/src/methods/transaction.spec.ts b/packages/data-schemas/src/methods/transaction.spec.ts index feaf9b758f..ee7df36c57 100644 --- a/packages/data-schemas/src/methods/transaction.spec.ts +++ b/packages/data-schemas/src/methods/transaction.spec.ts @@ -247,8 +247,8 @@ describe('Structured Token Spending Tests', () => { const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); - const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); - const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; // Act const result = await spendStructuredTokens(txData, tokenUsage); @@ -256,8 +256,8 @@ describe('Structured Token Spending Tests', () => { // Calculate expected costs. const expectedPromptCost = tokenUsage.promptTokens.input * promptMultiplier + - tokenUsage.promptTokens.write * (writeMultiplier ?? 0) + - tokenUsage.promptTokens.read * (readMultiplier ?? 0); + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; const expectedCompletionCost = tokenUsage.completionTokens * completionMultiplier; const expectedTotalCost = expectedPromptCost + expectedCompletionCost; const expectedBalance = initialBalance - expectedTotalCost; @@ -813,13 +813,18 @@ describe('Premium Token Pricing Integration Tests', () => { const premiumPromptRate = premiumTokenValues[model].prompt; const premiumCompletionRate = premiumTokenValues[model].completion; - const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); - const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: totalInput, + }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; const expectedPromptCost = tokenUsage.promptTokens.input * premiumPromptRate + - tokenUsage.promptTokens.write * (writeMultiplier ?? 0) + - tokenUsage.promptTokens.read * (readMultiplier ?? 0); + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; const expectedTotalCost = expectedPromptCost + expectedCompletionCost; @@ -859,13 +864,18 @@ describe('Premium Token Pricing Integration Tests', () => { const standardPromptRate = tokenValues[model].prompt; const standardCompletionRate = tokenValues[model].completion; - const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); - const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: totalInput, + }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; const expectedPromptCost = tokenUsage.promptTokens.input * standardPromptRate + - tokenUsage.promptTokens.write * (writeMultiplier ?? 0) + - tokenUsage.promptTokens.read * (readMultiplier ?? 0); + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; const expectedCompletionCost = tokenUsage.completionTokens * standardCompletionRate; const expectedTotalCost = expectedPromptCost + expectedCompletionCost; @@ -900,7 +910,7 @@ describe('Premium Token Pricing Integration Tests', () => { promptTokens * standardPromptRate + completionTokens * standardCompletionRate; const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendTokens should apply premium pricing for gemini-3.1-pro-preview above threshold', async () => { @@ -929,7 +939,7 @@ describe('Premium Token Pricing Integration Tests', () => { promptTokens * premiumPromptRate + completionTokens * premiumCompletionRate; const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendTokens should apply standard pricing for gemini-3.1-pro-preview at exactly the threshold', async () => { @@ -958,7 +968,7 @@ describe('Premium Token Pricing Integration Tests', () => { promptTokens * standardPromptRate + completionTokens * standardCompletionRate; const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); test('spendStructuredTokens should apply premium pricing for gemini-3.1 when total input exceeds threshold', async () => { @@ -992,8 +1002,13 @@ describe('Premium Token Pricing Integration Tests', () => { const premiumPromptRate = premiumTokenValues['gemini-3.1'].prompt; const premiumCompletionRate = premiumTokenValues['gemini-3.1'].completion; - const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); - const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: totalInput, + }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; const expectedPromptCost = tokenUsage.promptTokens.input * premiumPromptRate + @@ -1004,7 +1019,7 @@ describe('Premium Token Pricing Integration Tests', () => { const updatedBalance = await Balance.findOne({ user: userId }); expect(totalInput).toBeGreaterThan(premiumTokenValues['gemini-3.1'].threshold); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); }); test('non-premium models should not be affected by inputTokenCount regardless of prompt size', async () => { @@ -1036,339 +1051,3 @@ describe('Premium Token Pricing Integration Tests', () => { expect(updatedBalance?.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); }); - -describe('Bulk path parity', () => { - /** - * Each test here mirrors an existing legacy test above, replacing spendTokens/ - * spendStructuredTokens with recordCollectedUsage + bulk deps. - * The balance deduction and transaction document fields must be numerically identical. - */ - let bulkDeps; - let methods; - - beforeEach(() => { - methods = createMethods(mongoose); - bulkDeps = { - spendTokens: () => Promise.resolve(), - spendStructuredTokens: () => Promise.resolve(), - pricing: { getMultiplier, getCacheMultiplier }, - bulkWriteOps: { - insertMany: methods.bulkInsertTransactions, - updateBalance: methods.updateBalance, - }, - }; - }); - - test('balance should decrease when spending tokens via bulk path', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - const model = 'gpt-3.5-turbo'; - const promptTokens = 100; - const completionTokens = 50; - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-conversation-id', - model, - context: 'test', - balance: { enabled: true }, - transactions: { enabled: true }, - collectedUsage: [{ input_tokens: promptTokens, output_tokens: completionTokens, model }], - }); - - const updatedBalance = await Balance.findOne({ user: userId }); - const promptMultiplier = getMultiplier({ - model, - tokenType: 'prompt', - inputTokenCount: promptTokens, - }); - const completionMultiplier = getMultiplier({ - model, - tokenType: 'completion', - inputTokenCount: promptTokens, - }); - const expectedTotalCost = - promptTokens * promptMultiplier + completionTokens * completionMultiplier; - const expectedBalance = initialBalance - expectedTotalCost; - - expect(updatedBalance.tokenCredits).toBeCloseTo(expectedBalance, 0); - - const txns = await Transaction.find({ user: userId }).lean(); - expect(txns).toHaveLength(2); - }); - - test('bulk path should not update balance when balance.enabled is false', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - const model = 'gpt-3.5-turbo'; - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-conversation-id', - model, - context: 'test', - balance: { enabled: false }, - transactions: { enabled: true }, - collectedUsage: [{ input_tokens: 100, output_tokens: 50, model }], - }); - - const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBe(initialBalance); - const txns = await Transaction.find({ user: userId }).lean(); - expect(txns).toHaveLength(2); // transactions still recorded - }); - - test('bulk path should not insert when transactions.enabled is false', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-conversation-id', - model: 'gpt-3.5-turbo', - context: 'test', - balance: { enabled: true }, - transactions: { enabled: false }, - collectedUsage: [{ input_tokens: 100, output_tokens: 50, model: 'gpt-3.5-turbo' }], - }); - - const txns = await Transaction.find({ user: userId }).lean(); - expect(txns).toHaveLength(0); - const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); - }); - - test('bulk path handles incomplete context for completion tokens — same CANCEL_RATE as legacy', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 17613154.55; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - const model = 'claude-3-5-sonnet'; - const promptTokens = 10; - const completionTokens = 50; - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-convo', - model, - context: 'incomplete', - balance: { enabled: true }, - transactions: { enabled: true }, - collectedUsage: [{ input_tokens: promptTokens, output_tokens: completionTokens, model }], - }); - - const txns = await Transaction.find({ user: userId }).lean(); - const completionTx = txns.find((t) => t.tokenType === 'completion'); - const completionMultiplier = getMultiplier({ - model, - tokenType: 'completion', - inputTokenCount: promptTokens, - }); - expect(completionTx.tokenValue).toBeCloseTo(-completionTokens * completionMultiplier * 1.15, 0); - }); - - test('bulk path structured tokens — balance deduction matches legacy spendStructuredTokens', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 17613154.55; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - const model = 'claude-3-5-sonnet'; - const promptInput = 11; - const promptWrite = 140522; - const promptRead = 0; - const completionTokens = 5; - const totalInput = promptInput + promptWrite + promptRead; - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-convo', - model, - context: 'message', - balance: { enabled: true }, - transactions: { enabled: true }, - collectedUsage: [ - { - input_tokens: promptInput, - output_tokens: completionTokens, - model, - input_token_details: { cache_creation: promptWrite, cache_read: promptRead }, - }, - ], - }); - - const promptMultiplier = getMultiplier({ - model, - tokenType: 'prompt', - inputTokenCount: totalInput, - }); - const completionMultiplier = getMultiplier({ - model, - tokenType: 'completion', - inputTokenCount: totalInput, - }); - const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; - const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; - - const expectedPromptCost = - promptInput * promptMultiplier + promptWrite * writeMultiplier + promptRead * readMultiplier; - const expectedCompletionCost = completionTokens * completionMultiplier; - const expectedTotalCost = expectedPromptCost + expectedCompletionCost; - const expectedBalance = initialBalance - expectedTotalCost; - - const updatedBalance = await Balance.findOne({ user: userId }); - expect(Math.abs(updatedBalance.tokenCredits - expectedBalance)).toBeLessThan(100); - }); - - test('premium pricing above threshold via bulk path — same balance as legacy', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 100000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - const model = 'claude-opus-4-6'; - const promptTokens = 250000; - const completionTokens = 500; - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-premium', - model, - context: 'test', - balance: { enabled: true }, - transactions: { enabled: true }, - collectedUsage: [{ input_tokens: promptTokens, output_tokens: completionTokens, model }], - }); - - const premiumPromptRate = premiumTokenValues[model].prompt; - const premiumCompletionRate = premiumTokenValues[model].completion; - const expectedCost = - promptTokens * premiumPromptRate + completionTokens * premiumCompletionRate; - - const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); - }); - - test('real-world multi-entry batch: 5 sequential tool calls — same total deduction as 5 legacy spendTokens calls', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 100000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - const model = 'claude-opus-4-5-20251101'; - const calls = [ - { input_tokens: 31596, output_tokens: 151 }, - { input_tokens: 35368, output_tokens: 150 }, - { input_tokens: 58362, output_tokens: 295 }, - { input_tokens: 112604, output_tokens: 193 }, - { input_tokens: 257440, output_tokens: 2217 }, - ]; - - let expectedTotalCost = 0; - for (const { input_tokens, output_tokens } of calls) { - const pm = getMultiplier({ model, tokenType: 'prompt', inputTokenCount: input_tokens }); - const cm = getMultiplier({ model, tokenType: 'completion', inputTokenCount: input_tokens }); - expectedTotalCost += input_tokens * pm + output_tokens * cm; - } - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-sequential', - model, - context: 'message', - balance: { enabled: true }, - transactions: { enabled: true }, - collectedUsage: calls.map((c) => ({ ...c, model })), - }); - - const txns = await Transaction.find({ user: userId }).lean(); - expect(txns).toHaveLength(10); // 5 calls × 2 docs (prompt + completion) - - const updatedBalance = await Balance.findOne({ user: userId }); - expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); - }); - - test('bulk path should save transaction but not update balance when balance disabled, transactions enabled', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-conversation-id', - model: 'gpt-3.5-turbo', - context: 'test', - balance: { enabled: false }, - transactions: { enabled: true }, - collectedUsage: [{ input_tokens: 100, output_tokens: 50, model: 'gpt-3.5-turbo' }], - }); - - const txns = await Transaction.find({ user: userId }).lean(); - expect(txns).toHaveLength(2); - expect(txns[0].rawAmount).toBeDefined(); - const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); - }); - - test('bulk path structured tokens should not save when transactions.enabled is false', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-conversation-id', - model: 'claude-3-5-sonnet', - context: 'message', - balance: { enabled: true }, - transactions: { enabled: false }, - collectedUsage: [ - { - input_tokens: 10, - output_tokens: 5, - model: 'claude-3-5-sonnet', - input_token_details: { cache_creation: 100, cache_read: 5 }, - }, - ], - }); - - const txns = await Transaction.find({ user: userId }).lean(); - expect(txns).toHaveLength(0); - const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); - }); - - test('bulk path structured tokens should save but not update balance when balance disabled', async () => { - const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; - await Balance.create({ user: userId, tokenCredits: initialBalance }); - - await recordCollectedUsage(bulkDeps, { - user: userId.toString(), - conversationId: 'test-conversation-id', - model: 'claude-3-5-sonnet', - context: 'message', - balance: { enabled: false }, - transactions: { enabled: true }, - collectedUsage: [ - { - input_tokens: 10, - output_tokens: 5, - model: 'claude-3-5-sonnet', - input_token_details: { cache_creation: 100, cache_read: 5 }, - }, - ], - }); - - const txns = await Transaction.find({ user: userId }).lean(); - expect(txns).toHaveLength(2); - const promptTx = txns.find((t) => t.tokenType === 'prompt'); - expect(promptTx.inputTokens).toBe(-10); - expect(promptTx.writeTokens).toBe(-100); - expect(promptTx.readTokens).toBe(-5); - const balance = await Balance.findOne({ user: userId }); - expect(balance.tokenCredits).toBe(initialBalance); - }); -}); diff --git a/packages/data-schemas/src/methods/transaction.ts b/packages/data-schemas/src/methods/transaction.ts index 3f019defa2..66c34b7e00 100644 --- a/packages/data-schemas/src/methods/transaction.ts +++ b/packages/data-schemas/src/methods/transaction.ts @@ -1,7 +1,7 @@ import logger from '~/config/winston'; import type { FilterQuery, Model, Types } from 'mongoose'; +import type { IBalance, IBalanceUpdate, TransactionData } from '~/types'; import type { ITransaction } from '~/schema/transaction'; -import type { IBalance, IBalanceUpdate } from '~/types'; const cancelRate = 1.15; @@ -408,7 +408,22 @@ export function createTransactionMethods( return Balance.deleteMany(filter); } + async function bulkInsertTransactions(docs: TransactionData[]): Promise { + if (!docs.length) { + return; + } + try { + const Transaction = mongoose.models.Transaction; + await Transaction.insertMany(docs); + } catch (error) { + logger.error('[bulkInsertTransactions] Error inserting transaction docs:', error); + throw error; + } + } + return { + updateBalance, + bulkInsertTransactions, findBalanceByUser, upsertBalanceFields, getTransactions, diff --git a/packages/data-schemas/src/methods/tx.ts b/packages/data-schemas/src/methods/tx.ts index 23b22353dc..a1be4190ba 100644 --- a/packages/data-schemas/src/methods/tx.ts +++ b/packages/data-schemas/src/methods/tx.ts @@ -20,7 +20,10 @@ export interface TxDeps { /** From @librechat/api — matches a model name to a canonical key. */ matchModelName: (model: string, endpoint?: string) => string | undefined; /** From @librechat/api — finds the longest key in `values` whose key is a substring of `model`. */ - findMatchingPattern: (model: string, values: Record) => string | undefined; + findMatchingPattern: ( + model: string, + values: Record>, + ) => string | undefined; } export const defaultRate = 6; From 9e0592a236765f113c027587ccde0df1c2fa0690 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 7 Mar 2026 13:56:32 -0500 Subject: [PATCH 097/111] =?UTF-8?q?=F0=9F=93=9C=20feat:=20Implement=20Syst?= =?UTF-8?q?em=20Grants=20for=20Capability-Based=20Authorization=20(#11896)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Implement System Grants for Role-Based Capabilities - Added a new `systemGrant` model and associated methods to manage role-based capabilities within the application. - Introduced middleware functions `hasCapability` and `requireCapability` to check user permissions based on their roles. - Updated the database seeding process to include system grants for the ADMIN role, ensuring all necessary capabilities are assigned on startup. - Enhanced type definitions and schemas to support the new system grant functionality, improving overall type safety and clarity in the codebase. * test: Add unit tests for capabilities middleware and system grant methods - Introduced comprehensive unit tests for the capabilities middleware, including `hasCapability` and `requireCapability`, ensuring proper permission checks based on user roles. - Added tests for the `SystemGrant` methods, verifying the seeding of system grants, capability granting, and revocation processes. - Enhanced test coverage for edge cases, including idempotency of grant operations and handling of unexpected errors in middleware. - Utilized mocks for database interactions to isolate tests and improve reliability. * refactor: Transition to Capability-Based Access Control - Replaced role-based access checks with capability-based checks across various middleware and routes, enhancing permission management. - Introduced `hasCapability` and `requireCapability` functions to streamline capability verification for user actions. - Updated relevant routes and middleware to utilize the new capability system, ensuring consistent permission enforcement. - Enhanced type definitions and added tests for the new capability functions, improving overall code reliability and maintainability. * test: Enhance capability-based access tests for ADMIN role - Updated tests to reflect the new capability-based access control, specifically for the ADMIN role. - Modified test descriptions to clarify that users with the MANAGE_AGENTS capability can bypass permission checks. - Seeded capabilities for the ADMIN role in multiple test files to ensure consistent permission checks across different routes and middleware. - Improved overall test coverage for capability verification, ensuring robust permission management. * test: Update capability tests for MCP server access - Renamed test to reflect the correct capability for bypassing permission checks, changing from MANAGE_AGENTS to MANAGE_MCP_SERVERS. - Updated seeding of capabilities for the ADMIN role to align with the new capability structure. - Ensured consistency in capability definitions across tests and middleware for improved permission management. * feat: Add hasConfigCapability for enhanced config access control - Introduced `hasConfigCapability` function to check user permissions for managing or reading specific config sections. - Updated middleware to export the new capability function, ensuring consistent access control across the application. - Enhanced unit tests to cover various scenarios for the new capability, improving overall test coverage and reliability. * fix: Update tenantId filter in createSystemGrantMethods - Added a condition to set tenantId filter to { $exists: false } when tenantId is null, ensuring proper handling of cases where tenantId is not provided. - This change improves the robustness of the system grant methods by explicitly managing the absence of tenantId in the filter logic. * fix: account deletion capability check - Updated the `canDeleteAccount` middleware to ensure that the `hasManageUsers` capability check only occurs if a user is present, preventing potential errors when the user object is undefined. - This change improves the robustness of the account deletion logic by ensuring proper handling of user permissions. * refactor: Optimize seeding of system grants for ADMIN role - Replaced sequential capability granting with parallel execution using Promise.all in the seedSystemGrants function. - This change improves performance and efficiency during the initialization of system grants, ensuring all capabilities are granted concurrently. * refactor: Simplify systemGrantSchema index definition - Removed the sparse option from the unique index on principalType, principalId, capability, and tenantId in the systemGrantSchema. - This change streamlines the index definition, potentially improving query performance and clarity in the schema design. * refactor: Reorganize role capability check in roles route - Moved the capability check for reading roles to occur after parsing the roleName, improving code clarity and structure. - This change ensures that the authorization logic is consistently applied before fetching role details, enhancing overall permission management. * refactor: Remove unused ISystemGrant interface from systemCapabilities.ts - Deleted the ISystemGrant interface as it was no longer needed, streamlining the code and improving clarity. - This change helps reduce clutter in the file and focuses on relevant capabilities for the system. * refactor: Migrate SystemCapabilities to data-schemas - Replaced imports of SystemCapabilities from 'librechat-data-provider' with imports from '@librechat/data-schemas' across multiple files. - This change centralizes the management of system capabilities, improving code organization and maintainability. * refactor: Update account deletion middleware and capability checks - Modified the `canDeleteAccount` middleware to ensure that the account deletion permission is only granted to users with the `MANAGE_USERS` capability, improving security and clarity in permission management. - Enhanced error logging for unauthorized account deletion attempts, providing better insights into permission issues. - Updated the `capabilities.ts` file to ensure consistent handling of user authentication checks, improving robustness in capability verification. - Refined type definitions in `systemGrant.ts` and `systemGrantMethods.ts` to utilize the `PrincipalType` enum, enhancing type safety and code clarity. * refactor: Extract principal ID normalization into a separate function - Introduced `normalizePrincipalId` function to streamline the normalization of principal IDs based on their type, enhancing code clarity and reusability. - Updated references in `createSystemGrantMethods` to utilize the new normalization function, improving maintainability and reducing code duplication. * test: Add unit tests for principalId normalization in systemGrant - Introduced tests for the `grantCapability`, `revokeCapability`, and `getCapabilitiesForPrincipal` methods to verify correct handling of principalId normalization between string and ObjectId formats. - Enhanced the `capabilities.ts` middleware to utilize the `PrincipalType` enum for improved type safety. - Added a new utility function `normalizePrincipalId` to streamline principal ID normalization logic, ensuring consistent behavior across the application. * feat: Introduce capability implications and enhance system grant methods - Added `CapabilityImplications` to define relationships between broader and implied capabilities, allowing for more intuitive permission checks. - Updated `createSystemGrantMethods` to expand capability queries to include implied capabilities, improving authorization logic. - Enhanced `systemGrantSchema` to include an `expiresAt` field for future TTL enforcement of grants, and added validation to ensure `tenantId` is not set to null. - Documented authorization requirements for prompt group and prompt deletion methods to clarify access control expectations. * test: Add unit tests for canDeleteAccount middleware - Introduced unit tests for the `canDeleteAccount` middleware to verify account deletion permissions based on user roles and capabilities. - Covered scenarios for both allowed and blocked account deletions, including checks for ADMIN users with the `MANAGE_USERS` capability and handling of undefined user cases. - Enhanced test structure to ensure clarity and maintainability of permission checks in the middleware. * fix: Add principalType enum validation to SystemGrant schema Without enum validation, any string value was accepted for principalType and silently stored. Invalid documents would never match capability queries, creating phantom grants impossible to diagnose without raw DB inspection. All other ACL models in the codebase validate this field. * fix: Replace seedSystemGrants Promise.all with bulkWrite for concurrency safety When two server instances start simultaneously (K8s rolling deploy, PM2 cluster), both call seedSystemGrants. With Promise.all + findOneAndUpdate upsert, both instances may attempt to insert the same documents, causing E11000 duplicate key errors that crash server startup. bulkWrite with ordered:false handles concurrent upserts gracefully and reduces 17 individual round trips to a single network call. The returned documents (previously discarded) are no longer fetched. * perf: Add AsyncLocalStorage per-request cache for capability checks Every hasCapability call previously required 2 DB round trips (getUserPrincipals + SystemGrant.exists) — replacing what were O(1) string comparisons. Routes like patchPromptGroup triggered this twice, and hasConfigCapability's fallback path resolved principals twice. This adds a per-request AsyncLocalStorage cache that: - Caches resolved principals (same for all checks within one request) - Caches capability check results (same user+cap = same answer) - Automatically scoped to request lifetime (no stale grants) - Falls through to DB when no store exists (background jobs, tests) - Requires no signature changes to hasCapability The capabilityContextMiddleware is registered at the app level before all routes, initializing a fresh store per request. * fix: Add error handling for inline hasCapability calls canDeleteAccount, fetchAssistants, and validateAuthor all call hasCapability without try-catch. These were previously O(1) string comparisons that could never throw. Now they hit the database and can fail on connection timeout or transient errors. Wrap each call in try-catch, defaulting to deny (false) on error. This ensures a DB hiccup returns a clean 403 instead of an unhandled 500 with a stack trace. * test: Add canDeleteAccount DB-error resilience test Tests that hasCapability rejection (e.g., DB timeout) results in a clean 403 rather than an unhandled exception. Validates the error handling added in the previous commit. * refactor: Use barrel import for hasCapability in validateAuthor Import from ~/server/middleware barrel instead of directly from ~/server/middleware/roles/capabilities for consistency with other non-middleware consumers. Files within the middleware barrel itself must continue using direct imports to avoid circular requires. * refactor: Remove misleading pre('save') hook from SystemGrant schema The pre('save') hook normalized principalId for USER/GROUP principals, but the primary write path (grantCapability) uses findOneAndUpdate — which does not trigger save hooks. The normalization was already handled explicitly in grantCapability itself. The hook created a false impression of schema-level enforcement that only covered save()/create() paths. Replace with a comment documenting that all writes must go through grantCapability. * feat: Add READ_ASSISTANTS capability to complete manage/read pair Every other managed resource had a paired READ_X / MANAGE_X capability except assistants. This adds READ_ASSISTANTS and registers the MANAGE_ASSISTANTS → READ_ASSISTANTS implication in CapabilityImplications, enabling future read-only assistant visibility grants. * chore: Reorder systemGrant methods for clarity Moved hasCapabilityForPrincipals to a more logical position in the returned object of createSystemGrantMethods, improving code readability. This change also maintains the inclusion of seedSystemGrants in the export, ensuring all necessary methods are available. * fix: Wrap seedSystemGrants in try-catch to avoid blocking startup Seeding capabilities is idempotent and will succeed on the next restart. A transient DB error during seeding should not prevent the server from starting — log the error and continue. * refactor: Improve capability check efficiency and add audit logging Move hasCapability calls after cheap early-exits in validateAuthor and fetchAssistants so the DB check only runs when its result matters. Add logger.debug on every capability bypass grant across all 7 call sites for auditability, and log errors in catch blocks instead of silently swallowing them. * test: Add integration tests for AsyncLocalStorage capability caching Exercises the full vertical — ALS context, generateCapabilityCheck, real getUserPrincipals, real hasCapabilityForPrincipals, real MongoDB via MongoMemoryServer. Covers per-request caching, cross-context isolation, concurrent request isolation, negative caching, capability implications, tenant scoping, group-based grants, and requireCapability middleware. * test: Add systemGrant data-layer and ALS edge-case integration tests systemGrant.spec.ts (51 tests): Full integration tests for all systemGrant methods against real MongoDB — grant/revoke lifecycle, principalId normalization (string→ObjectId for USER/GROUP, string for ROLE), capability implications (both directions), tenant scoping, schema validation (null tenantId, invalid enum, required fields, unique compound index). capabilities.integration.spec.ts (27 tests): Adds ALS edge cases — missing context degrades gracefully with no caching (background jobs, child processes), nested middleware creates independent inner context, optional-chaining safety when store is undefined, mid-request grant changes are invisible due to result caching, requireCapability works without ALS, and interleaved concurrent contexts maintain isolation. * fix: Add worker thread guards to capability ALS usage Detect when hasCapability or capabilityContextMiddleware is called from a worker thread (where ALS context does not propagate from the parent). hasCapability logs a warn-once per factory instance; the middleware logs an error since mounting Express middleware in a worker is likely a misconfiguration. Both continue to function correctly — the guard is observability, not a hard block. * fix: Include tenantId in ALS principal cache key for tenant isolation The principal cache key was user.id:user.role, which would reuse cached principals across tenants for the same user within a request. When getUserPrincipals gains tenant-scoped group resolution, principals from tenant-a would incorrectly serve tenant-b checks. Changed to user.id:user.role:user.tenantId to prevent cross-tenant cache hits. Adds integration test proving separate principal lookups per tenantId. * test: Remove redundant mocked capabilities.spec.js The JS wrapper test (7 tests, all mocked) is a strict subset of capabilities.integration.spec.ts (28 tests, real MongoDB). Every scenario it covered — hasCapability true/false, tenantId passthrough, requireCapability 403/500, error handling — is tested with higher fidelity in the integration suite. * test: Replace mocked canDeleteAccount tests with real MongoDB integration Remove hasCapability mock — tests now exercise the full capability chain against real MongoDB (getUserPrincipals, hasCapabilityForPrincipals, SystemGrant collection). Only mocks remaining are logger and cache. Adds new coverage: admin role without grant is blocked, user-level grant bypasses deletion restriction, null user handling. * test: Add comprehensive tests for ACL entry management and user group methods Introduces new tests for `deleteAclEntries`, `bulkWriteAclEntries`, and `findPublicResourceIds` in `aclEntry.spec.ts`, ensuring proper functionality for deleting and bulk managing ACL entries. Additionally, enhances `userGroup.spec.ts` with tests for finding groups by ID and name pattern, including external ID matching and source filtering. These changes improve coverage and validate the integrity of ACL and user group operations against real MongoDB interactions. * refactor: Update capability checks and logging for better clarity and error handling Replaced `MANAGE_USERS` with `ACCESS_ADMIN` in the `canDeleteAccount` middleware and related tests to align with updated permission structure. Enhanced logging in various middleware functions to use `logger.warn` for capability check failures, providing clearer error messages. Additionally, refactored capability checks in the `patchPromptGroup` and `validateAuthor` functions to improve readability and maintainability. This commit also includes adjustments to the `systemGrant` methods to implement retry logic for transient failures during capability seeding, ensuring robustness in the face of database errors. * refactor: Enhance logging and retry logic in seedSystemGrants method Updated the logging format in the seedSystemGrants method to include error messages for better clarity. Improved the retry mechanism by explicitly mocking multiple failures in tests, ensuring robust error handling during transient database issues. Additionally, refined imports in the systemGrant schema for better type management. * refactor: Consolidate imports in canDeleteAccount middleware Merged logger and SystemCapabilities imports from the data-schemas module into a single line for improved readability and maintainability of the code. This change streamlines the import statements in the canDeleteAccount middleware. * test: Enhance systemGrant tests for error handling and capability validation Added tests to the systemGrant methods to handle various error scenarios, including E11000 race conditions, invalid ObjectId strings for USER and GROUP principals, and invalid capability strings. These enhancements improve the robustness of the capability granting and revoking logic, ensuring proper error propagation and validation of inputs. * fix: Wrap hasCapability calls in deny-by-default try-catch at remaining sites canAccessResource, files.js, and roles.js all had hasCapability inside outer try-catch blocks that returned 500 on DB failure instead of falling through to the regular ACL check. This contradicts the deny-by-default pattern used everywhere else. Also removes raw error.message from the roles.js 500 response to prevent internal host/connection info leaking to clients. * fix: Normalize user ID in canDeleteAccount before passing to hasCapability requireCapability normalizes req.user.id via _id?.toString() fallback, but canDeleteAccount passed raw req.user directly. If req.user.id is absent (some auth layers only populate _id), getUserPrincipals received undefined, silently returning empty principals and blocking the bypass. * fix: Harden systemGrant schema and type safety - Reject empty string tenantId in schema validator (was only blocking null; empty string silently orphaned documents) - Fix reverseImplications to use BaseSystemCapability[] instead of string[], preserving the narrow discriminated type - Document READ_ASSISTANTS as reserved/unenforced * test: Use fake timers for seedSystemGrants retry tests and add tenantId validation - Switch retry tests to jest.useFakeTimers() to eliminate 3+ seconds of real setTimeout delays per test run - Add regression test for empty-string tenantId rejection * docs: Add TODO(#12091) comments for tenant-scoped capability gaps In multi-tenant mode, platform-level grants (no tenantId) won't match tenant-scoped queries, breaking admin access. getUserPrincipals also returns cross-tenant group memberships. Both need fixes in #12091. --- api/models/index.js | 1 + api/server/controllers/assistants/helpers.js | 17 +- api/server/index.js | 10 +- .../canAccessMCPServerResource.spec.js | 13 +- .../accessResources/canAccessResource.js | 17 +- .../middleware/assistants/validateAuthor.js | 19 +- api/server/middleware/canDeleteAccount.js | 29 +- .../middleware/canDeleteAccount.spec.js | 180 +++ api/server/middleware/roles/capabilities.js | 14 + api/server/middleware/roles/index.js | 10 + api/server/routes/admin/auth.js | 17 +- api/server/routes/files/files.agents.test.js | 13 +- api/server/routes/files/files.js | 29 +- api/server/routes/prompts.js | 28 +- api/server/routes/prompts.test.js | 21 +- api/server/routes/roles.js | 38 +- api/server/services/systemGrant.spec.js | 407 ++++++ .../capabilities.integration.spec.ts | 659 ++++++++++ .../api/src/middleware/capabilities.spec.ts | 212 +++ packages/api/src/middleware/capabilities.ts | 188 +++ packages/api/src/middleware/index.ts | 1 + packages/data-schemas/src/index.ts | 1 + .../data-schemas/src/methods/aclEntry.spec.ts | 281 ++++ packages/data-schemas/src/methods/index.ts | 6 + .../data-schemas/src/methods/prompt.spec.ts | 2 - packages/data-schemas/src/methods/prompt.ts | 34 +- .../src/methods/systemGrant.spec.ts | 840 ++++++++++++ .../data-schemas/src/methods/systemGrant.ts | 266 ++++ .../src/methods/userGroup.spec.ts | 1140 ++++++++++------- .../data-schemas/src/methods/userGroup.ts | 7 + packages/data-schemas/src/models/index.ts | 2 + .../data-schemas/src/models/systemGrant.ts | 11 + packages/data-schemas/src/schema/index.ts | 1 + .../data-schemas/src/schema/systemGrant.ts | 76 ++ .../data-schemas/src/systemCapabilities.ts | 106 ++ packages/data-schemas/src/types/index.ts | 1 + .../data-schemas/src/types/systemGrant.ts | 25 + packages/data-schemas/src/utils/index.ts | 1 + packages/data-schemas/src/utils/principal.ts | 22 + 39 files changed, 4207 insertions(+), 538 deletions(-) create mode 100644 api/server/middleware/canDeleteAccount.spec.js create mode 100644 api/server/middleware/roles/capabilities.js create mode 100644 api/server/services/systemGrant.spec.js create mode 100644 packages/api/src/middleware/capabilities.integration.spec.ts create mode 100644 packages/api/src/middleware/capabilities.spec.ts create mode 100644 packages/api/src/middleware/capabilities.ts create mode 100644 packages/data-schemas/src/methods/systemGrant.spec.ts create mode 100644 packages/data-schemas/src/methods/systemGrant.ts create mode 100644 packages/data-schemas/src/models/systemGrant.ts create mode 100644 packages/data-schemas/src/schema/systemGrant.ts create mode 100644 packages/data-schemas/src/systemCapabilities.ts create mode 100644 packages/data-schemas/src/types/systemGrant.ts create mode 100644 packages/data-schemas/src/utils/principal.ts diff --git a/api/models/index.js b/api/models/index.js index 03d5d3ec71..2a1cb222f9 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -13,6 +13,7 @@ const seedDatabase = async () => { await methods.initializeRoles(); await methods.seedDefaultRoles(); await methods.ensureDefaultCategories(); + await methods.seedSystemGrants(); }; module.exports = { diff --git a/api/server/controllers/assistants/helpers.js b/api/server/controllers/assistants/helpers.js index 9183680f1e..6309268770 100644 --- a/api/server/controllers/assistants/helpers.js +++ b/api/server/controllers/assistants/helpers.js @@ -1,14 +1,15 @@ const { - SystemRoles, EModelEndpoint, defaultOrderQuery, defaultAssistantsVersion, } = require('librechat-data-provider'); +const { logger, SystemCapabilities } = require('@librechat/data-schemas'); const { initializeClient: initAzureClient, } = require('~/server/services/Endpoints/azureAssistants'); const { initializeClient } = require('~/server/services/Endpoints/assistants'); const { getEndpointsConfig } = require('~/server/services/Config'); +const { hasCapability } = require('~/server/middleware'); /** * @param {ServerRequest} req @@ -236,9 +237,19 @@ const fetchAssistants = async ({ req, res, overrideEndpoint }) => { body = await listAssistantsForAzure({ req, res, version, azureConfig, query }); } - if (req.user.role === SystemRoles.ADMIN) { + if (!appConfig.endpoints?.[endpoint]) { return body; - } else if (!appConfig.endpoints?.[endpoint]) { + } + + let canManageAssistants = false; + try { + canManageAssistants = await hasCapability(req.user, SystemCapabilities.MANAGE_ASSISTANTS); + } catch (err) { + logger.warn(`[fetchAssistants] capability check failed, denying bypass: ${err.message}`); + } + + if (canManageAssistants) { + logger.debug(`[fetchAssistants] MANAGE_ASSISTANTS bypass for user ${req.user.id}`); return body; } diff --git a/api/server/index.js b/api/server/index.js index 6af829eab8..ba376ab335 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -20,13 +20,14 @@ const { GenerationJobManager, createStreamServices, initializeFileStorage, + updateInterfacePermissions, } = require('@librechat/api'); const { connectDb, indexSync } = require('~/db'); const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager'); +const { getRoleByName, updateAccessPermissions, seedDatabase } = require('~/models'); +const { capabilityContextMiddleware } = require('./middleware/roles/capabilities'); const createValidateImageRequest = require('./middleware/validateImageRequest'); const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies'); -const { updateInterfacePermissions: updateInterfacePerms } = require('@librechat/api'); -const { getRoleByName, updateAccessPermissions, seedDatabase } = require('~/models'); const { checkMigrations } = require('./services/start/migration'); const initializeMCPs = require('./services/initializeMCPs'); const configureSocialLogins = require('./socialLogins'); @@ -62,7 +63,7 @@ const startServer = async () => { const appConfig = await getAppConfig(); initializeFileStorage(appConfig); await performStartupChecks(appConfig); - await updateInterfacePerms({ appConfig, getRoleByName, updateAccessPermissions }); + await updateInterfacePermissions({ appConfig, getRoleByName, updateAccessPermissions }); const indexPath = path.join(appConfig.paths.dist, 'index.html'); let indexHTML = fs.readFileSync(indexPath, 'utf8'); @@ -133,6 +134,9 @@ const startServer = async () => { await configureSocialLogins(app); } + /* Per-request capability cache — must be registered before any route that calls hasCapability */ + app.use(capabilityContextMiddleware); + app.use('/oauth', routes.oauth); /* API Endpoints */ app.use('/api/auth', routes.auth); diff --git a/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js b/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js index 77508be2d1..6f7e4ab506 100644 --- a/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js +++ b/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js @@ -1,8 +1,9 @@ const mongoose = require('mongoose'); const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider'); +const { SystemCapabilities } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { canAccessMCPServerResource } = require('./canAccessMCPServerResource'); -const { User, Role, AclEntry } = require('~/db/models'); +const { User, Role, AclEntry, SystemGrant } = require('~/db/models'); const { createMCPServer } = require('~/models'); describe('canAccessMCPServerResource middleware', () => { @@ -511,7 +512,7 @@ describe('canAccessMCPServerResource middleware', () => { }); }); - test('should allow admin users to bypass permission checks', async () => { + test('should allow users with MANAGE_MCP_SERVERS capability to bypass permission checks', async () => { const { SystemRoles } = require('librechat-data-provider'); // Create an MCP server owned by another user @@ -531,6 +532,14 @@ describe('canAccessMCPServerResource middleware', () => { author: otherUser._id, }); + // Seed MANAGE_MCP_SERVERS capability for the ADMIN role + await SystemGrant.create({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + capability: SystemCapabilities.MANAGE_MCP_SERVERS, + grantedAt: new Date(), + }); + // Set user as admin req.user = { id: testUser._id, role: SystemRoles.ADMIN }; req.params.serverName = mcpServer.serverName; diff --git a/api/server/middleware/accessResources/canAccessResource.js b/api/server/middleware/accessResources/canAccessResource.js index c8bd15ffc2..2431971b2f 100644 --- a/api/server/middleware/accessResources/canAccessResource.js +++ b/api/server/middleware/accessResources/canAccessResource.js @@ -1,5 +1,5 @@ -const { logger } = require('@librechat/data-schemas'); -const { SystemRoles } = require('librechat-data-provider'); +const { logger, ResourceCapabilityMap } = require('@librechat/data-schemas'); +const { hasCapability } = require('~/server/middleware/roles/capabilities'); const { checkPermission } = require('~/server/services/PermissionService'); /** @@ -71,8 +71,17 @@ const canAccessResource = (options) => { message: 'Authentication required', }); } - // if system admin let through - if (req.user.role === SystemRoles.ADMIN) { + const cap = ResourceCapabilityMap[resourceType]; + let hasCap = false; + try { + hasCap = cap != null && (await hasCapability(req.user, cap)); + } catch (err) { + logger.warn(`[canAccessResource] capability check failed, denying bypass: ${err.message}`); + } + if (hasCap) { + logger.debug( + `[canAccessResource] ${cap} bypass for user ${req.user.id} on ${resourceType} ${rawResourceId}`, + ); return next(); } const userId = req.user.id; diff --git a/api/server/middleware/assistants/validateAuthor.js b/api/server/middleware/assistants/validateAuthor.js index 6c15704251..3be1642a71 100644 --- a/api/server/middleware/assistants/validateAuthor.js +++ b/api/server/middleware/assistants/validateAuthor.js @@ -1,4 +1,5 @@ -const { SystemRoles } = require('librechat-data-provider'); +const { logger, SystemCapabilities } = require('@librechat/data-schemas'); +const { hasCapability } = require('~/server/middleware'); const { getAssistant } = require('~/models'); /** @@ -12,10 +13,6 @@ const { getAssistant } = require('~/models'); * @returns {Promise} */ const validateAuthor = async ({ req, openai, overrideEndpoint, overrideAssistantId }) => { - if (req.user.role === SystemRoles.ADMIN) { - return; - } - const endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint; const assistant_id = overrideAssistantId ?? req.params.id ?? req.body.assistant_id ?? req.query.assistant_id; @@ -31,6 +28,18 @@ const validateAuthor = async ({ req, openai, overrideEndpoint, overrideAssistant return; } + let canManageAssistants = false; + try { + canManageAssistants = await hasCapability(req.user, SystemCapabilities.MANAGE_ASSISTANTS); + } catch (err) { + logger.warn(`[validateAuthor] capability check failed, denying bypass: ${err.message}`); + } + + if (canManageAssistants) { + logger.debug(`[validateAuthor] MANAGE_ASSISTANTS bypass for user ${req.user.id}`); + return; + } + const assistantDoc = await getAssistant({ assistant_id, user: req.user.id }); if (assistantDoc) { return; diff --git a/api/server/middleware/canDeleteAccount.js b/api/server/middleware/canDeleteAccount.js index a913495287..3c08745d76 100644 --- a/api/server/middleware/canDeleteAccount.js +++ b/api/server/middleware/canDeleteAccount.js @@ -1,6 +1,6 @@ const { isEnabled } = require('@librechat/api'); -const { logger } = require('@librechat/data-schemas'); -const { SystemRoles } = require('librechat-data-provider'); +const { logger, SystemCapabilities } = require('@librechat/data-schemas'); +const { hasCapability } = require('~/server/middleware/roles/capabilities'); /** * Checks if the user can delete their account @@ -17,12 +17,29 @@ const { SystemRoles } = require('librechat-data-provider'); const canDeleteAccount = async (req, res, next = () => {}) => { const { user } = req; const { ALLOW_ACCOUNT_DELETION = true } = process.env; - if (user?.role === SystemRoles.ADMIN || isEnabled(ALLOW_ACCOUNT_DELETION)) { + if (isEnabled(ALLOW_ACCOUNT_DELETION)) { return next(); - } else { - logger.error(`[User] [Delete Account] [User cannot delete account] [User: ${user?.id}]`); - return res.status(403).send({ message: 'You do not have permission to delete this account' }); } + let hasAdminAccess = false; + if (user) { + try { + const id = user.id ?? user._id?.toString(); + if (id) { + hasAdminAccess = await hasCapability( + { id, role: user.role ?? '', tenantId: user.tenantId }, + SystemCapabilities.ACCESS_ADMIN, + ); + } + } catch (err) { + logger.warn(`[canDeleteAccount] capability check failed, denying: ${err.message}`); + } + } + if (hasAdminAccess) { + logger.debug(`[canDeleteAccount] ACCESS_ADMIN bypass for user ${user.id}`); + return next(); + } + logger.error(`[User] [Delete Account] [User cannot delete account] [User: ${user?.id}]`); + return res.status(403).send({ message: 'You do not have permission to delete this account' }); }; module.exports = canDeleteAccount; diff --git a/api/server/middleware/canDeleteAccount.spec.js b/api/server/middleware/canDeleteAccount.spec.js new file mode 100644 index 0000000000..abb888c4a4 --- /dev/null +++ b/api/server/middleware/canDeleteAccount.spec.js @@ -0,0 +1,180 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { SystemRoles, PrincipalType } = require('librechat-data-provider'); +const { SystemCapabilities } = require('@librechat/data-schemas'); + +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { error: jest.fn(), warn: jest.fn(), debug: jest.fn(), info: jest.fn() }, +})); + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(() => ({ + get: jest.fn(), + set: jest.fn(), + })), +})); + +const { User, SystemGrant } = require('~/db/models'); +const canDeleteAccount = require('./canDeleteAccount'); + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await mongoose.connection.dropDatabase(); + delete process.env.ALLOW_ACCOUNT_DELETION; +}); + +const makeRes = () => { + const send = jest.fn(); + const status = jest.fn().mockReturnValue({ send }); + return { status, send }; +}; + +describe('canDeleteAccount', () => { + describe('ALLOW_ACCOUNT_DELETION=true (default)', () => { + it('calls next without hitting the DB', async () => { + process.env.ALLOW_ACCOUNT_DELETION = 'true'; + const next = jest.fn(); + const req = { user: { id: 'user-1', role: SystemRoles.USER } }; + + await canDeleteAccount(req, makeRes(), next); + + expect(next).toHaveBeenCalled(); + }); + + it('skips capability check entirely when deletion is allowed', async () => { + process.env.ALLOW_ACCOUNT_DELETION = 'true'; + const next = jest.fn(); + const req = { user: { id: 'user-1', role: SystemRoles.USER } }; + + await canDeleteAccount(req, makeRes(), next); + + expect(next).toHaveBeenCalled(); + const grantCount = await SystemGrant.countDocuments(); + expect(grantCount).toBe(0); + }); + }); + + describe('ALLOW_ACCOUNT_DELETION=false', () => { + beforeEach(() => { + process.env.ALLOW_ACCOUNT_DELETION = 'false'; + }); + + it('allows admin with ACCESS_ADMIN grant (real DB check)', async () => { + const admin = await User.create({ + name: 'Admin', + email: 'admin@test.com', + password: 'password123', + provider: 'local', + role: SystemRoles.ADMIN, + }); + + await SystemGrant.create({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + capability: SystemCapabilities.ACCESS_ADMIN, + grantedAt: new Date(), + }); + + const next = jest.fn(); + const req = { user: { id: admin._id.toString(), role: SystemRoles.ADMIN } }; + + await canDeleteAccount(req, makeRes(), next); + + expect(next).toHaveBeenCalled(); + }); + + it('blocks regular user without ACCESS_ADMIN grant', async () => { + const user = await User.create({ + name: 'Regular', + email: 'user@test.com', + password: 'password123', + provider: 'local', + role: SystemRoles.USER, + }); + + const next = jest.fn(); + const res = makeRes(); + const req = { user: { id: user._id.toString(), role: SystemRoles.USER } }; + + await canDeleteAccount(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('blocks admin role WITHOUT the ACCESS_ADMIN grant', async () => { + const admin = await User.create({ + name: 'Admin No Grant', + email: 'admin2@test.com', + password: 'password123', + provider: 'local', + role: SystemRoles.ADMIN, + }); + + const next = jest.fn(); + const res = makeRes(); + const req = { user: { id: admin._id.toString(), role: SystemRoles.ADMIN } }; + + await canDeleteAccount(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('allows user-level grant (not just role-level)', async () => { + const user = await User.create({ + name: 'Privileged User', + email: 'priv@test.com', + password: 'password123', + provider: 'local', + role: SystemRoles.USER, + }); + + await SystemGrant.create({ + principalType: PrincipalType.USER, + principalId: user._id, + capability: SystemCapabilities.ACCESS_ADMIN, + grantedAt: new Date(), + }); + + const next = jest.fn(); + const req = { user: { id: user._id.toString(), role: SystemRoles.USER } }; + + await canDeleteAccount(req, makeRes(), next); + + expect(next).toHaveBeenCalled(); + }); + + it('blocks when user is undefined — does not throw', async () => { + const next = jest.fn(); + const res = makeRes(); + + await canDeleteAccount({ user: undefined }, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('blocks when user is null — does not throw', async () => { + const next = jest.fn(); + const res = makeRes(); + + await canDeleteAccount({ user: null }, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + }); + }); +}); diff --git a/api/server/middleware/roles/capabilities.js b/api/server/middleware/roles/capabilities.js new file mode 100644 index 0000000000..6f2aa43e96 --- /dev/null +++ b/api/server/middleware/roles/capabilities.js @@ -0,0 +1,14 @@ +const { generateCapabilityCheck, capabilityContextMiddleware } = require('@librechat/api'); +const { getUserPrincipals, hasCapabilityForPrincipals } = require('~/models'); + +const { hasCapability, requireCapability, hasConfigCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals, +}); + +module.exports = { + hasCapability, + requireCapability, + hasConfigCapability, + capabilityContextMiddleware, +}; diff --git a/api/server/middleware/roles/index.js b/api/server/middleware/roles/index.js index f01b884e5a..e6c315d007 100644 --- a/api/server/middleware/roles/index.js +++ b/api/server/middleware/roles/index.js @@ -1,5 +1,15 @@ +const { + hasCapability, + requireCapability, + hasConfigCapability, + capabilityContextMiddleware, +} = require('./capabilities'); const checkAdmin = require('./admin'); module.exports = { checkAdmin, + hasCapability, + requireCapability, + hasConfigCapability, + capabilityContextMiddleware, }; diff --git a/api/server/routes/admin/auth.js b/api/server/routes/admin/auth.js index e729f20940..e19adf54a9 100644 --- a/api/server/routes/admin/auth.js +++ b/api/server/routes/admin/auth.js @@ -3,20 +3,19 @@ const passport = require('passport'); const { randomState } = require('openid-client'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys } = require('librechat-data-provider'); -const { - requireAdmin, - getAdminPanelUrl, - exchangeAdminCode, - createSetBalanceConfig, -} = require('@librechat/api'); +const { SystemCapabilities } = require('@librechat/data-schemas'); +const { getAdminPanelUrl, exchangeAdminCode, createSetBalanceConfig } = require('@librechat/api'); const { loginController } = require('~/server/controllers/auth/LoginController'); const { createOAuthHandler } = require('~/server/controllers/auth/oauth'); const { findBalanceByUser, upsertBalanceFields } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); +const { requireCapability } = require('~/server/middleware'); const getLogStores = require('~/cache/getLogStores'); const { getOpenIdConfig } = require('~/strategies'); const middleware = require('~/server/middleware'); +const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN); + const setBalanceConfig = createSetBalanceConfig({ getAppConfig, findBalanceByUser, @@ -31,12 +30,12 @@ router.post( middleware.loginLimiter, middleware.checkBan, middleware.requireLocalAuth, - requireAdmin, + requireAdminAccess, setBalanceConfig, loginController, ); -router.get('/verify', middleware.requireJwtAuth, requireAdmin, (req, res) => { +router.get('/verify', middleware.requireJwtAuth, requireAdminAccess, (req, res) => { const { password: _p, totpSecret: _t, __v, ...user } = req.user; user.id = user._id.toString(); res.status(200).json({ user }); @@ -67,7 +66,7 @@ router.get( failureMessage: true, session: false, }), - requireAdmin, + requireAdminAccess, setBalanceConfig, middleware.checkDomainAllowed, createOAuthHandler(`${getAdminPanelUrl()}/auth/openid/callback`), diff --git a/api/server/routes/files/files.agents.test.js b/api/server/routes/files/files.agents.test.js index 203c1210fd..e64be9cf4e 100644 --- a/api/server/routes/files/files.agents.test.js +++ b/api/server/routes/files/files.agents.test.js @@ -10,6 +10,7 @@ const { ResourceType, PrincipalType, } = require('librechat-data-provider'); +const { SystemCapabilities } = require('@librechat/data-schemas'); const { createAgent, createFile } = require('~/models'); // Only mock the external dependencies that we don't want to test @@ -82,6 +83,7 @@ describe('File Routes - Agent Files Endpoint', () => { let AclEntry; // eslint-disable-next-line no-unused-vars let AccessRole; + let SystemGrant; let modelsToCleanup = []; beforeAll(async () => { @@ -108,6 +110,7 @@ describe('File Routes - Agent Files Endpoint', () => { AclEntry = models.AclEntry; User = models.User; AccessRole = models.AccessRole; + SystemGrant = models.SystemGrant; // Seed default roles using our methods await methods.seedDefaultRoles(); @@ -532,7 +535,7 @@ describe('File Routes - Agent Files Endpoint', () => { expect(processAgentFileUpload).not.toHaveBeenCalled(); }); - it('should allow file upload for admin user regardless of agent ownership', async () => { + it('should allow file upload for user with MANAGE_AGENTS capability regardless of agent ownership', async () => { // Create an agent owned by authorId await createAgent({ id: agentCustomId, @@ -542,6 +545,14 @@ describe('File Routes - Agent Files Endpoint', () => { author: authorId, }); + // Seed MANAGE_AGENTS capability for the ADMIN role + await SystemGrant.create({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + capability: SystemCapabilities.MANAGE_AGENTS, + grantedAt: new Date(), + }); + // Create app with admin user (otherUserId as admin) const testApp = createAppWithUser(otherUserId, SystemRoles.ADMIN); diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index a51c00f26e..5578fc6474 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -14,6 +14,7 @@ const { checkOpenAIStorage, isAssistantsEndpoint, } = require('librechat-data-provider'); +const { SystemCapabilities } = require('@librechat/data-schemas'); const { filterFile, processFileUpload, @@ -28,6 +29,7 @@ const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud'); const { hasAccessToFilesViaAgent } = require('~/server/services/Files'); const { cleanFileName } = require('~/server/utils/files'); +const { hasCapability } = require('~/server/middleware'); const { getLogStores } = require('~/cache'); const { Readable } = require('stream'); const db = require('~/models'); @@ -379,15 +381,24 @@ router.post('/', async (req, res) => { return await processFileUpload({ req, res, metadata }); } - const denied = await verifyAgentUploadPermission({ - req, - res, - metadata, - getAgent: db.getAgent, - checkPermission, - }); - if (denied) { - return; + let skipUploadAuth = false; + try { + skipUploadAuth = await hasCapability(req.user, SystemCapabilities.MANAGE_AGENTS); + } catch (err) { + logger.warn(`[/files] capability check failed, denying bypass: ${err.message}`); + } + + if (!skipUploadAuth) { + const denied = await verifyAgentUploadPermission({ + req, + res, + metadata, + getAgent: db.getAgent, + checkPermission, + }); + if (denied) { + return; + } } return await processAgentFileUpload({ req, res, metadata }); diff --git a/api/server/routes/prompts.js b/api/server/routes/prompts.js index d437273df2..c2e15ac6c0 100644 --- a/api/server/routes/prompts.js +++ b/api/server/routes/prompts.js @@ -11,13 +11,13 @@ const { } = require('@librechat/api'); const { Permissions, - SystemRoles, ResourceType, AccessRoleIds, PrincipalType, PermissionBits, PermissionTypes, } = require('librechat-data-provider'); +const { SystemCapabilities } = require('@librechat/data-schemas'); const { getListPromptGroupsByAccess, makePromptProduction, @@ -32,6 +32,7 @@ const { getPrompt, } = require('~/models'); const { + hasCapability, canAccessPromptGroupResource, canAccessPromptViaGroup, requireJwtAuth, @@ -333,7 +334,14 @@ const patchPromptGroup = async (req, res) => { const { groupId } = req.params; const author = req.user.id; const filter = { _id: groupId, author }; - if (req.user.role === SystemRoles.ADMIN) { + let canManagePrompts = false; + try { + canManagePrompts = await hasCapability(req.user, SystemCapabilities.MANAGE_PROMPTS); + } catch (err) { + logger.warn(`[patchPromptGroup] capability check failed, denying bypass: ${err.message}`); + } + if (canManagePrompts) { + logger.debug(`[patchPromptGroup] MANAGE_PROMPTS bypass for user ${req.user.id}`); delete filter.author; } @@ -421,7 +429,14 @@ router.get('/', async (req, res) => { // If no groupId, return user's own prompts const query = { author }; - if (req.user.role === SystemRoles.ADMIN) { + let canReadPrompts = false; + try { + canReadPrompts = await hasCapability(req.user, SystemCapabilities.READ_PROMPTS); + } catch (err) { + logger.warn(`[GET /prompts] capability check failed, denying bypass: ${err.message}`); + } + if (canReadPrompts) { + logger.debug(`[GET /prompts] READ_PROMPTS bypass for user ${req.user.id}`); delete query.author; } const prompts = await getPrompts(query); @@ -445,8 +460,7 @@ const deletePromptController = async (req, res) => { try { const { promptId } = req.params; const { groupId } = req.query; - const author = req.user.id; - const query = { promptId, groupId, author, role: req.user.role }; + const query = { promptId, groupId }; const result = await deletePrompt(query); res.status(200).send(result); } catch (error) { @@ -464,8 +478,8 @@ const deletePromptController = async (req, res) => { const deletePromptGroupController = async (req, res) => { try { const { groupId: _id } = req.params; - // Don't pass author - permissions are now checked by middleware - const message = await deletePromptGroup({ _id, role: req.user.role }); + // Don't pass author or role - permissions are checked by ACL middleware + const message = await deletePromptGroup({ _id }); res.send(message); } catch (error) { logger.error('Error deleting prompt group', error); diff --git a/api/server/routes/prompts.test.js b/api/server/routes/prompts.test.js index 80c973147f..ec162ac1fb 100644 --- a/api/server/routes/prompts.test.js +++ b/api/server/routes/prompts.test.js @@ -10,6 +10,7 @@ const { PrincipalType, PermissionBits, } = require('librechat-data-provider'); +const { SystemCapabilities } = require('@librechat/data-schemas'); // Mock modules before importing jest.mock('~/server/services/Config', () => ({ @@ -35,6 +36,7 @@ jest.mock('~/models', () => { jest.mock('~/server/middleware', () => ({ requireJwtAuth: (req, res, next) => next(), + hasCapability: jest.requireActual('~/server/middleware').hasCapability, canAccessPromptViaGroup: jest.requireActual('~/server/middleware').canAccessPromptViaGroup, canAccessPromptGroupResource: jest.requireActual('~/server/middleware').canAccessPromptGroupResource, @@ -43,7 +45,7 @@ jest.mock('~/server/middleware', () => ({ let app; let mongoServer; let promptRoutes; -let Prompt, PromptGroup, AclEntry, AccessRole, User; +let Prompt, PromptGroup, AclEntry, AccessRole, User, SystemGrant; let testUsers, testRoles; let grantPermission; let currentTestUser; // Track current user for middleware @@ -65,6 +67,7 @@ beforeAll(async () => { AclEntry = dbModels.AclEntry; AccessRole = dbModels.AccessRole; User = dbModels.User; + SystemGrant = dbModels.SystemGrant; // Import permission service const permissionService = require('~/server/services/PermissionService'); @@ -165,6 +168,22 @@ async function setupTestData() { }), }; + // Seed capabilities for the ADMIN role + await SystemGrant.create([ + { + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + capability: SystemCapabilities.MANAGE_PROMPTS, + grantedAt: new Date(), + }, + { + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + capability: SystemCapabilities.READ_PROMPTS, + grantedAt: new Date(), + }, + ]); + // Mock getRoleByName const { getRoleByName } = require('~/models'); getRoleByName.mockImplementation((roleName) => { diff --git a/api/server/routes/roles.js b/api/server/routes/roles.js index 4c0f044f76..1b7e4632e3 100644 --- a/api/server/routes/roles.js +++ b/api/server/routes/roles.js @@ -1,4 +1,5 @@ const express = require('express'); +const { logger, SystemCapabilities } = require('@librechat/data-schemas'); const { SystemRoles, roleDefaults, @@ -11,11 +12,12 @@ const { peoplePickerPermissionsSchema, remoteAgentsPermissionsSchema, } = require('librechat-data-provider'); -const { checkAdmin, requireJwtAuth } = require('~/server/middleware'); +const { hasCapability, requireCapability, requireJwtAuth } = require('~/server/middleware'); const { updateRoleByName, getRoleByName } = require('~/models'); const router = express.Router(); router.use(requireJwtAuth); +const manageRoles = requireCapability(SystemCapabilities.MANAGE_ROLES); /** * Permission configuration mapping @@ -111,14 +113,17 @@ router.get('/:roleName', async (req, res) => { // TODO: TEMP, use a better parsing for roleName const roleName = _r.toUpperCase(); - if ( - (req.user.role !== SystemRoles.ADMIN && roleName === SystemRoles.ADMIN) || - (req.user.role !== SystemRoles.ADMIN && !roleDefaults[roleName]) - ) { - return res.status(403).send({ message: 'Unauthorized' }); - } - try { + let hasReadRoles = false; + try { + hasReadRoles = await hasCapability(req.user, SystemCapabilities.READ_ROLES); + } catch (err) { + logger.warn(`[GET /roles/:roleName] capability check failed: ${err.message}`); + } + if (!hasReadRoles && (roleName === SystemRoles.ADMIN || !roleDefaults[roleName])) { + return res.status(403).send({ message: 'Unauthorized' }); + } + const role = await getRoleByName(roleName, '-_id -__v'); if (!role) { return res.status(404).send({ message: 'Role not found' }); @@ -126,7 +131,8 @@ router.get('/:roleName', async (req, res) => { res.status(200).send(role); } catch (error) { - return res.status(500).send({ message: 'Failed to retrieve role', error: error.message }); + logger.error('[GET /roles/:roleName] Error:', error); + return res.status(500).send({ message: 'Failed to retrieve role' }); } }); @@ -134,42 +140,42 @@ router.get('/:roleName', async (req, res) => { * PUT /api/roles/:roleName/prompts * Update prompt permissions for a specific role */ -router.put('/:roleName/prompts', checkAdmin, createPermissionUpdateHandler('prompts')); +router.put('/:roleName/prompts', manageRoles, createPermissionUpdateHandler('prompts')); /** * PUT /api/roles/:roleName/agents * Update agent permissions for a specific role */ -router.put('/:roleName/agents', checkAdmin, createPermissionUpdateHandler('agents')); +router.put('/:roleName/agents', manageRoles, createPermissionUpdateHandler('agents')); /** * PUT /api/roles/:roleName/memories * Update memory permissions for a specific role */ -router.put('/:roleName/memories', checkAdmin, createPermissionUpdateHandler('memories')); +router.put('/:roleName/memories', manageRoles, createPermissionUpdateHandler('memories')); /** * PUT /api/roles/:roleName/people-picker * Update people picker permissions for a specific role */ -router.put('/:roleName/people-picker', checkAdmin, createPermissionUpdateHandler('people-picker')); +router.put('/:roleName/people-picker', manageRoles, createPermissionUpdateHandler('people-picker')); /** * PUT /api/roles/:roleName/mcp-servers * Update MCP servers permissions for a specific role */ -router.put('/:roleName/mcp-servers', checkAdmin, createPermissionUpdateHandler('mcp-servers')); +router.put('/:roleName/mcp-servers', manageRoles, createPermissionUpdateHandler('mcp-servers')); /** * PUT /api/roles/:roleName/marketplace * Update marketplace permissions for a specific role */ -router.put('/:roleName/marketplace', checkAdmin, createPermissionUpdateHandler('marketplace')); +router.put('/:roleName/marketplace', manageRoles, createPermissionUpdateHandler('marketplace')); /** * PUT /api/roles/:roleName/remote-agents * Update remote agents (API) permissions for a specific role */ -router.put('/:roleName/remote-agents', checkAdmin, createPermissionUpdateHandler('remote-agents')); +router.put('/:roleName/remote-agents', manageRoles, createPermissionUpdateHandler('remote-agents')); module.exports = router; diff --git a/api/server/services/systemGrant.spec.js b/api/server/services/systemGrant.spec.js new file mode 100644 index 0000000000..4e10ee5641 --- /dev/null +++ b/api/server/services/systemGrant.spec.js @@ -0,0 +1,407 @@ +const mongoose = require('mongoose'); +const { createModels, createMethods } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { SystemRoles, PrincipalType } = require('librechat-data-provider'); +const { SystemCapabilities } = require('@librechat/data-schemas'); + +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + getTransactionSupport: jest.fn().mockResolvedValue(false), + createModels: jest.requireActual('@librechat/data-schemas').createModels, + createMethods: jest.requireActual('@librechat/data-schemas').createMethods, +})); + +jest.mock('~/server/services/GraphApiService', () => ({ + entraIdPrincipalFeatureEnabled: jest.fn().mockReturnValue(false), + getUserOwnedEntraGroups: jest.fn().mockResolvedValue([]), + getUserEntraGroups: jest.fn().mockResolvedValue([]), + getGroupMembers: jest.fn().mockResolvedValue([]), + getGroupOwners: jest.fn().mockResolvedValue([]), +})); + +jest.mock('~/config', () => ({ + logger: { error: jest.fn() }, +})); + +let mongoServer; +let methods; +let SystemGrant; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + + createModels(mongoose); + const dbModels = require('~/db/models'); + Object.assign(mongoose.models, dbModels); + SystemGrant = dbModels.SystemGrant; + + methods = createMethods(mongoose, { + matchModelName: () => null, + findMatchingPattern: () => null, + getCache: () => ({ + get: async () => null, + set: async () => {}, + }), + }); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await SystemGrant.deleteMany({}); +}); + +describe('SystemGrant methods', () => { + describe('seedSystemGrants', () => { + it('seeds all capabilities for the ADMIN role', async () => { + await methods.seedSystemGrants(); + + const grants = await SystemGrant.find({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + }).lean(); + + const expectedCount = Object.values(SystemCapabilities).length; + expect(grants).toHaveLength(expectedCount); + + const capabilities = grants.map((g) => g.capability).sort(); + const expected = Object.values(SystemCapabilities).sort(); + expect(capabilities).toEqual(expected); + }); + + it('is idempotent — calling twice does not duplicate grants', async () => { + await methods.seedSystemGrants(); + await methods.seedSystemGrants(); + + const count = await SystemGrant.countDocuments({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + }); + + expect(count).toBe(Object.values(SystemCapabilities).length); + }); + + it('seeds grants with no tenantId', async () => { + await methods.seedSystemGrants(); + + const withTenant = await SystemGrant.countDocuments({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + tenantId: { $exists: true }, + }); + + expect(withTenant).toBe(0); + }); + }); + + describe('grantCapability / revokeCapability', () => { + it('grants a capability to a user', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + const grant = await SystemGrant.findOne({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }).lean(); + + expect(grant).toBeTruthy(); + expect(grant.grantedAt).toBeInstanceOf(Date); + }); + + it('upsert does not create duplicates', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + const count = await SystemGrant.countDocuments({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + expect(count).toBe(1); + }); + + it('revokes a capability', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + await methods.revokeCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + const grant = await SystemGrant.findOne({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }).lean(); + + expect(grant).toBeNull(); + }); + }); + + describe('hasCapabilityForPrincipals', () => { + it('returns true when role principal has the capability', async () => { + await methods.seedSystemGrants(); + + const principals = [ + { principalType: PrincipalType.USER, principalId: new mongoose.Types.ObjectId() }, + { principalType: PrincipalType.ROLE, principalId: SystemRoles.ADMIN }, + { principalType: PrincipalType.PUBLIC }, + ]; + + const result = await methods.hasCapabilityForPrincipals({ + principals, + capability: SystemCapabilities.ACCESS_ADMIN, + }); + + expect(result).toBe(true); + }); + + it('returns false when no principal has the capability', async () => { + const principals = [ + { principalType: PrincipalType.USER, principalId: new mongoose.Types.ObjectId() }, + { principalType: PrincipalType.ROLE, principalId: SystemRoles.USER }, + { principalType: PrincipalType.PUBLIC }, + ]; + + const result = await methods.hasCapabilityForPrincipals({ + principals, + capability: SystemCapabilities.ACCESS_ADMIN, + }); + + expect(result).toBe(false); + }); + + it('returns false for an empty principals list', async () => { + const result = await methods.hasCapabilityForPrincipals({ + principals: [], + capability: SystemCapabilities.ACCESS_ADMIN, + }); + + expect(result).toBe(false); + }); + + it('ignores PUBLIC principals', async () => { + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.PUBLIC }], + capability: SystemCapabilities.ACCESS_ADMIN, + }); + + expect(result).toBe(false); + }); + + it('matches user-level grants', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + }); + + const principals = [ + { principalType: PrincipalType.USER, principalId: userId }, + { principalType: PrincipalType.ROLE, principalId: SystemRoles.USER }, + { principalType: PrincipalType.PUBLIC }, + ]; + + const result = await methods.hasCapabilityForPrincipals({ + principals, + capability: SystemCapabilities.READ_CONFIGS, + }); + + expect(result).toBe(true); + }); + + it('matches group-level grants', async () => { + const groupId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.GROUP, + principalId: groupId, + capability: SystemCapabilities.READ_USAGE, + }); + + const principals = [ + { principalType: PrincipalType.USER, principalId: new mongoose.Types.ObjectId() }, + { principalType: PrincipalType.GROUP, principalId: groupId }, + { principalType: PrincipalType.PUBLIC }, + ]; + + const result = await methods.hasCapabilityForPrincipals({ + principals, + capability: SystemCapabilities.READ_USAGE, + }); + + expect(result).toBe(true); + }); + }); + + describe('getCapabilitiesForPrincipal', () => { + it('lists all capabilities for a principal', async () => { + await methods.seedSystemGrants(); + + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + }); + + expect(grants).toHaveLength(Object.values(SystemCapabilities).length); + }); + + it('returns empty array for a principal with no grants', async () => { + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.USER, + }); + + expect(grants).toHaveLength(0); + }); + }); + + describe('principalId normalization', () => { + it('grant with string userId is found by hasCapabilityForPrincipals with ObjectId', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId.toString(), // string input + capability: SystemCapabilities.READ_USAGE, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], // ObjectId input + capability: SystemCapabilities.READ_USAGE, + }); + + expect(result).toBe(true); + }); + + it('revoke with string userId removes the grant stored as ObjectId', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId.toString(), + capability: SystemCapabilities.READ_USAGE, + }); + + await methods.revokeCapability({ + principalType: PrincipalType.USER, + principalId: userId.toString(), // string revoke + capability: SystemCapabilities.READ_USAGE, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_USAGE, + }); + + expect(result).toBe(false); + }); + + it('getCapabilitiesForPrincipal with string userId returns grants stored as ObjectId', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId.toString(), + capability: SystemCapabilities.READ_USAGE, + }); + + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.USER, + principalId: userId.toString(), // string lookup + }); + + expect(grants).toHaveLength(1); + expect(grants[0].capability).toBe(SystemCapabilities.READ_USAGE); + }); + }); + + describe('tenant scoping', () => { + it('tenant-scoped grant does not match platform-level query', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_CONFIGS, + }); + + expect(result).toBe(false); + }); + + it('tenant-scoped grant matches same-tenant query', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + + expect(result).toBe(true); + }); + + it('tenant-scoped grant does not match different tenant', async () => { + const userId = new mongoose.Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-2', + }); + + expect(result).toBe(false); + }); + }); +}); diff --git a/packages/api/src/middleware/capabilities.integration.spec.ts b/packages/api/src/middleware/capabilities.integration.spec.ts new file mode 100644 index 0000000000..dee1f446e6 --- /dev/null +++ b/packages/api/src/middleware/capabilities.integration.spec.ts @@ -0,0 +1,659 @@ +import mongoose, { Types } from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { PrincipalType, SystemRoles } from 'librechat-data-provider'; +import { + createModels, + createMethods, + SystemCapabilities, + CapabilityImplications, +} from '@librechat/data-schemas'; +import type { SystemCapability } from '@librechat/data-schemas'; +import type { AllMethods } from '@librechat/data-schemas'; +import { + generateCapabilityCheck, + capabilityStore, + capabilityContextMiddleware, +} from './capabilities'; + +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + }, +})); + +let mongoServer: MongoMemoryServer; +let methods: AllMethods; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + createModels(mongoose); + methods = createMethods(mongoose); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await mongoose.connection.dropDatabase(); +}); + +/** + * Runs `fn` inside an AsyncLocalStorage context identical to what + * capabilityContextMiddleware sets up for real Express requests. + */ +function withinRequestContext(fn: () => Promise): Promise { + return new Promise((resolve, reject) => { + capabilityContextMiddleware( + {} as Parameters[0], + {} as Parameters[1], + () => { + fn().then(resolve, reject); + }, + ); + }); +} + +describe('capabilities integration (real MongoDB)', () => { + let adminUser: { _id: Types.ObjectId; id: string; role: string }; + let regularUser: { _id: Types.ObjectId; id: string; role: string }; + + beforeEach(async () => { + const User = mongoose.models.User; + + const admin = await User.create({ + name: 'Admin', + email: 'admin@test.com', + password: 'password123', + provider: 'local', + role: SystemRoles.ADMIN, + }); + adminUser = { _id: admin._id, id: admin._id.toString(), role: SystemRoles.ADMIN }; + + const user = await User.create({ + name: 'Regular', + email: 'user@test.com', + password: 'password123', + provider: 'local', + role: SystemRoles.USER, + }); + regularUser = { _id: user._id, id: user._id.toString(), role: SystemRoles.USER }; + }); + + describe('end-to-end with real getUserPrincipals + hasCapabilityForPrincipals', () => { + let hasCapability: ReturnType['hasCapability']; + let hasConfigCapability: ReturnType['hasConfigCapability']; + + beforeEach(() => { + ({ hasCapability, hasConfigCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + })); + }); + + it('returns true for ADMIN after seedSystemGrants', async () => { + await methods.seedSystemGrants(); + + const result = await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + expect(result).toBe(true); + }); + + it('returns false for regular USER (no grants)', async () => { + await methods.seedSystemGrants(); + + const result = await hasCapability(regularUser, SystemCapabilities.ACCESS_ADMIN); + expect(result).toBe(false); + }); + + it('resolves all seeded capabilities for ADMIN', async () => { + await methods.seedSystemGrants(); + + for (const cap of Object.values(SystemCapabilities)) { + const result = await hasCapability(adminUser, cap); + expect(result).toBe(true); + } + }); + + it('resolves capability implications (MANAGE_X implies READ_X)', async () => { + await methods.grantCapability({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.USER, + capability: SystemCapabilities.MANAGE_USERS, + }); + + const hasManage = await hasCapability(regularUser, SystemCapabilities.MANAGE_USERS); + const hasRead = await hasCapability(regularUser, SystemCapabilities.READ_USERS); + + expect(hasManage).toBe(true); + expect(hasRead).toBe(true); + }); + + it('implication is one-directional (READ_X does NOT imply MANAGE_X)', async () => { + await methods.grantCapability({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.USER, + capability: SystemCapabilities.READ_USERS, + }); + + const hasRead = await hasCapability(regularUser, SystemCapabilities.READ_USERS); + const hasManage = await hasCapability(regularUser, SystemCapabilities.MANAGE_USERS); + + expect(hasRead).toBe(true); + expect(hasManage).toBe(false); + }); + + it('grants to a specific user work independently of role', async () => { + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: regularUser.id, + capability: SystemCapabilities.READ_AGENTS, + }); + + const result = await hasCapability(regularUser, SystemCapabilities.READ_AGENTS); + expect(result).toBe(true); + }); + + it('grants via group membership are resolved', async () => { + const Group = mongoose.models.Group; + const group = await Group.create({ + name: 'Editors', + source: 'local', + memberIds: [regularUser.id], + }); + + await methods.grantCapability({ + principalType: PrincipalType.GROUP, + principalId: group._id, + capability: SystemCapabilities.MANAGE_PROMPTS, + }); + + const result = await hasCapability(regularUser, SystemCapabilities.MANAGE_PROMPTS); + expect(result).toBe(true); + }); + + it('revoked capability is no longer granted', async () => { + await methods.grantCapability({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.USER, + capability: SystemCapabilities.READ_USAGE, + }); + expect(await hasCapability(regularUser, SystemCapabilities.READ_USAGE)).toBe(true); + + await methods.revokeCapability({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.USER, + capability: SystemCapabilities.READ_USAGE, + }); + expect(await hasCapability(regularUser, SystemCapabilities.READ_USAGE)).toBe(false); + }); + + it('tenant-scoped grant does not leak to platform-level check', async () => { + await methods.grantCapability({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.USER, + capability: SystemCapabilities.ACCESS_ADMIN, + tenantId: 'tenant-a', + }); + + const platformResult = await hasCapability(regularUser, SystemCapabilities.ACCESS_ADMIN); + expect(platformResult).toBe(false); + + const tenantResult = await hasCapability( + { ...regularUser, tenantId: 'tenant-a' }, + SystemCapabilities.ACCESS_ADMIN, + ); + expect(tenantResult).toBe(true); + }); + + it('hasConfigCapability falls back to section-specific grant', async () => { + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: regularUser.id, + capability: 'manage:configs:endpoints' as SystemCapability, + }); + + const hasBroad = await hasConfigCapability(regularUser, 'endpoints'); + expect(hasBroad).toBe(true); + + const hasOtherSection = await hasConfigCapability(regularUser, 'balance'); + expect(hasOtherSection).toBe(false); + }); + }); + + describe('AsyncLocalStorage per-request caching', () => { + it('caches getUserPrincipals within a single request context', async () => { + await methods.seedSystemGrants(); + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + await hasCapability(adminUser, SystemCapabilities.MANAGE_USERS); + await hasCapability(adminUser, SystemCapabilities.READ_CONFIGS); + }); + + expect(getUserPrincipals).toHaveBeenCalledTimes(1); + }); + + it('caches capability results within a single request context', async () => { + await methods.seedSystemGrants(); + const hasCapabilityForPrincipals = jest.fn(methods.hasCapabilityForPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + const r1 = await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + const r2 = await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + expect(r1).toBe(true); + expect(r2).toBe(true); + }); + + const accessAdminCalls = hasCapabilityForPrincipals.mock.calls.filter( + (args) => args[0].capability === SystemCapabilities.ACCESS_ADMIN, + ); + expect(accessAdminCalls).toHaveLength(1); + }); + + it('does NOT share cache across separate request contexts', async () => { + await methods.seedSystemGrants(); + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + }); + + await withinRequestContext(async () => { + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + }); + + expect(getUserPrincipals).toHaveBeenCalledTimes(2); + }); + + it('isolates cache between concurrent request contexts', async () => { + await methods.seedSystemGrants(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: regularUser.id, + capability: SystemCapabilities.READ_AGENTS, + }); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + const results = await Promise.all([ + withinRequestContext(async () => { + const admin = await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + const agents = await hasCapability(adminUser, SystemCapabilities.READ_AGENTS); + return { admin, agents, who: 'admin' }; + }), + withinRequestContext(async () => { + const admin = await hasCapability(regularUser, SystemCapabilities.ACCESS_ADMIN); + const agents = await hasCapability(regularUser, SystemCapabilities.READ_AGENTS); + return { admin, agents, who: 'regular' }; + }), + ]); + + const adminResult = results.find((r) => r.who === 'admin')!; + const regularResult = results.find((r) => r.who === 'regular')!; + + expect(adminResult.admin).toBe(true); + expect(adminResult.agents).toBe(true); + expect(regularResult.admin).toBe(false); + expect(regularResult.agents).toBe(true); + }); + + it('falls through to DB when outside request context (no ALS)', async () => { + await methods.seedSystemGrants(); + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + + expect(getUserPrincipals).toHaveBeenCalledTimes(2); + }); + + it('caches false results correctly (negative caching)', async () => { + const hasCapabilityForPrincipals = jest.fn(methods.hasCapabilityForPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + const r1 = await hasCapability(regularUser, SystemCapabilities.MANAGE_USERS); + const r2 = await hasCapability(regularUser, SystemCapabilities.MANAGE_USERS); + expect(r1).toBe(false); + expect(r2).toBe(false); + }); + + const manageUserCalls = hasCapabilityForPrincipals.mock.calls.filter( + (args) => args[0].capability === SystemCapabilities.MANAGE_USERS, + ); + expect(manageUserCalls).toHaveLength(1); + }); + + it('uses separate principal cache keys for different users in same context', async () => { + await methods.seedSystemGrants(); + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + await hasCapability(regularUser, SystemCapabilities.ACCESS_ADMIN); + }); + + expect(getUserPrincipals).toHaveBeenCalledTimes(2); + }); + + it('uses separate principal cache keys for different tenantIds (same user)', async () => { + await methods.seedSystemGrants(); + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + await hasCapability( + { ...adminUser, tenantId: 'tenant-a' }, + SystemCapabilities.ACCESS_ADMIN, + ); + }); + + expect(getUserPrincipals).toHaveBeenCalledTimes(2); + }); + }); + + describe('requireCapability middleware (real DB, real ALS)', () => { + it('calls next() for granted capability inside request context', async () => { + await methods.seedSystemGrants(); + + const { requireCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + const middleware = requireCapability(SystemCapabilities.ACCESS_ADMIN); + const next = jest.fn(); + const jsonMock = jest.fn(); + const statusMock = jest.fn().mockReturnValue({ json: jsonMock }); + const req = { user: { id: adminUser.id, role: adminUser.role } }; + const res = { status: statusMock }; + + await withinRequestContext(async () => { + await middleware(req as never, res as never, next); + }); + + expect(next).toHaveBeenCalled(); + expect(statusMock).not.toHaveBeenCalled(); + }); + + it('returns 403 for denied capability inside request context', async () => { + await methods.seedSystemGrants(); + + const { requireCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + const middleware = requireCapability(SystemCapabilities.MANAGE_USERS); + const next = jest.fn(); + const jsonMock = jest.fn(); + const statusMock = jest.fn().mockReturnValue({ json: jsonMock }); + const req = { user: { id: regularUser.id, role: regularUser.role } }; + const res = { status: statusMock }; + + await withinRequestContext(async () => { + await middleware(req as never, res as never, next); + }); + + expect(next).not.toHaveBeenCalled(); + expect(statusMock).toHaveBeenCalledWith(403); + }); + }); + + describe('ALS edge cases', () => { + it('returns correct results when ALS context is missing (background job / child process)', async () => { + await methods.seedSystemGrants(); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + expect(capabilityStore.getStore()).toBeUndefined(); + + const adminResult = await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + const userResult = await hasCapability(regularUser, SystemCapabilities.ACCESS_ADMIN); + + expect(adminResult).toBe(true); + expect(userResult).toBe(false); + }); + + it('every DB call executes (no caching) when ALS context is missing', async () => { + await methods.seedSystemGrants(); + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + const hasCapabilityForPrincipals = jest.fn(methods.hasCapabilityForPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals, + }); + + expect(capabilityStore.getStore()).toBeUndefined(); + + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + await hasCapability(adminUser, SystemCapabilities.MANAGE_USERS); + + expect(getUserPrincipals).toHaveBeenCalledTimes(3); + expect(hasCapabilityForPrincipals).toHaveBeenCalledTimes(3); + }); + + it('nested capabilityContextMiddleware creates an independent inner context', async () => { + await methods.seedSystemGrants(); + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + + await withinRequestContext(async () => { + await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + }); + + await hasCapability(adminUser, SystemCapabilities.MANAGE_USERS); + }); + + /** + * Outer context: 1 call (ACCESS_ADMIN) — principals cached, MANAGE_USERS reuses them. + * Inner context: 1 call (ACCESS_ADMIN) — fresh context, no cache from outer. + * Total: 2 getUserPrincipals calls. + */ + expect(getUserPrincipals).toHaveBeenCalledTimes(2); + }); + + it('store.results.set with undefined store is a no-op (optional chaining safety)', async () => { + await methods.seedSystemGrants(); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + expect(capabilityStore.getStore()).toBeUndefined(); + + await expect(hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN)).resolves.toBe(true); + }); + + it('grant change mid-request is invisible due to result caching', async () => { + await methods.seedSystemGrants(); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + await withinRequestContext(async () => { + const before = await hasCapability(regularUser, SystemCapabilities.READ_AGENTS); + expect(before).toBe(false); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: regularUser.id, + capability: SystemCapabilities.READ_AGENTS, + }); + + const after = await hasCapability(regularUser, SystemCapabilities.READ_AGENTS); + expect(after).toBe(false); + }); + + const afterContext = await hasCapability(regularUser, SystemCapabilities.READ_AGENTS); + expect(afterContext).toBe(true); + }); + + it('requireCapability works correctly without ALS context', async () => { + await methods.seedSystemGrants(); + + const { requireCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + expect(capabilityStore.getStore()).toBeUndefined(); + + const middleware = requireCapability(SystemCapabilities.ACCESS_ADMIN); + const next = jest.fn(); + const jsonMock = jest.fn(); + const statusMock = jest.fn().mockReturnValue({ json: jsonMock }); + const req = { user: { id: adminUser.id, role: adminUser.role } }; + const res = { status: statusMock }; + + await middleware(req as never, res as never, next); + + expect(next).toHaveBeenCalled(); + expect(statusMock).not.toHaveBeenCalled(); + }); + + it('concurrent contexts with interleaved awaits maintain isolation', async () => { + await methods.seedSystemGrants(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: regularUser.id, + capability: SystemCapabilities.READ_AGENTS, + }); + + const getUserPrincipals = jest.fn(methods.getUserPrincipals); + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + let adminResolve: () => void; + const adminGate = new Promise((r) => { + adminResolve = r; + }); + + let userResolve: () => void; + const userGate = new Promise((r) => { + userResolve = r; + }); + + const adminPromise = withinRequestContext(async () => { + const r1 = await hasCapability(adminUser, SystemCapabilities.ACCESS_ADMIN); + adminResolve!(); + await userGate; + const r2 = await hasCapability(adminUser, SystemCapabilities.READ_AGENTS); + return { r1, r2 }; + }); + + const userPromise = withinRequestContext(async () => { + await adminGate; + const r1 = await hasCapability(regularUser, SystemCapabilities.ACCESS_ADMIN); + userResolve!(); + const r2 = await hasCapability(regularUser, SystemCapabilities.READ_AGENTS); + return { r1, r2 }; + }); + + const [adminResults, userResults] = await Promise.all([adminPromise, userPromise]); + + expect(adminResults.r1).toBe(true); + expect(adminResults.r2).toBe(true); + expect(userResults.r1).toBe(false); + expect(userResults.r2).toBe(true); + + expect(getUserPrincipals).toHaveBeenCalledTimes(2); + }); + }); + + describe('CapabilityImplications consistency', () => { + it('every implication pair resolves correctly through the full stack', async () => { + const pairs = Object.entries(CapabilityImplications) as [ + SystemCapability, + SystemCapability[], + ][]; + + const { hasCapability } = generateCapabilityCheck({ + getUserPrincipals: methods.getUserPrincipals, + hasCapabilityForPrincipals: methods.hasCapabilityForPrincipals, + }); + + for (const [broadCap, impliedCaps] of pairs) { + await mongoose.connection.dropDatabase(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: regularUser.id, + capability: broadCap, + }); + + for (const impliedCap of impliedCaps) { + const result = await hasCapability(regularUser, impliedCap); + expect(result).toBe(true); + } + + const hasBroad = await hasCapability(regularUser, broadCap); + expect(hasBroad).toBe(true); + } + }); + }); +}); diff --git a/packages/api/src/middleware/capabilities.spec.ts b/packages/api/src/middleware/capabilities.spec.ts new file mode 100644 index 0000000000..75d3142369 --- /dev/null +++ b/packages/api/src/middleware/capabilities.spec.ts @@ -0,0 +1,212 @@ +import { PrincipalType } from 'librechat-data-provider'; +import { + configCapability, + SystemCapabilities, + readConfigCapability, +} from '@librechat/data-schemas'; +import type { Response } from 'express'; +import type { ServerRequest } from '~/types/http'; +import { generateCapabilityCheck } from './capabilities'; + +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { + error: jest.fn(), + }, +})); + +const adminPrincipals = [ + { principalType: PrincipalType.USER, principalId: 'user-123' }, + { principalType: PrincipalType.ROLE, principalId: 'ADMIN' }, + { principalType: PrincipalType.PUBLIC }, +]; + +const userPrincipals = [ + { principalType: PrincipalType.USER, principalId: 'user-456' }, + { principalType: PrincipalType.ROLE, principalId: 'USER' }, + { principalType: PrincipalType.PUBLIC }, +]; + +describe('generateCapabilityCheck', () => { + const mockGetUserPrincipals = jest.fn(); + const mockHasCapabilityForPrincipals = jest.fn(); + + const { hasCapability, requireCapability, hasConfigCapability } = generateCapabilityCheck({ + getUserPrincipals: mockGetUserPrincipals, + hasCapabilityForPrincipals: mockHasCapabilityForPrincipals, + }); + + beforeEach(() => { + mockGetUserPrincipals.mockReset(); + mockHasCapabilityForPrincipals.mockReset(); + }); + + describe('hasCapability', () => { + it('returns true for a user with the capability', async () => { + mockGetUserPrincipals.mockResolvedValue(adminPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValue(true); + + const result = await hasCapability( + { id: 'user-123', role: 'ADMIN' }, + SystemCapabilities.ACCESS_ADMIN, + ); + + expect(result).toBe(true); + expect(mockGetUserPrincipals).toHaveBeenCalledWith({ userId: 'user-123', role: 'ADMIN' }); + expect(mockHasCapabilityForPrincipals).toHaveBeenCalledWith({ + principals: adminPrincipals, + capability: SystemCapabilities.ACCESS_ADMIN, + tenantId: undefined, + }); + }); + + it('returns false for a user without the capability', async () => { + mockGetUserPrincipals.mockResolvedValue(userPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValue(false); + + const result = await hasCapability( + { id: 'user-456', role: 'USER' }, + SystemCapabilities.MANAGE_USERS, + ); + + expect(result).toBe(false); + }); + + it('passes tenantId when present on user', async () => { + mockGetUserPrincipals.mockResolvedValue(adminPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValue(true); + + await hasCapability( + { id: 'user-123', role: 'ADMIN', tenantId: 'tenant-1' }, + SystemCapabilities.READ_CONFIGS, + ); + + expect(mockHasCapabilityForPrincipals).toHaveBeenCalledWith( + expect.objectContaining({ tenantId: 'tenant-1' }), + ); + }); + }); + + describe('requireCapability', () => { + let mockReq: Partial; + let mockRes: Partial; + let mockNext: jest.Mock; + let jsonMock: jest.Mock; + let statusMock: jest.Mock; + + beforeEach(() => { + jsonMock = jest.fn(); + statusMock = jest.fn().mockReturnValue({ json: jsonMock }); + + mockReq = { + user: { id: 'user-123', role: 'ADMIN' } as ServerRequest['user'], + }; + mockRes = { status: statusMock }; + mockNext = jest.fn(); + }); + + it('calls next() when user has the capability', async () => { + mockGetUserPrincipals.mockResolvedValue(adminPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValue(true); + + const middleware = requireCapability(SystemCapabilities.ACCESS_ADMIN); + await middleware(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(statusMock).not.toHaveBeenCalled(); + }); + + it('returns 403 when user lacks the capability', async () => { + mockReq.user = { id: 'user-456', role: 'USER' } as ServerRequest['user']; + mockGetUserPrincipals.mockResolvedValue(userPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValue(false); + + const middleware = requireCapability(SystemCapabilities.MANAGE_USERS); + await middleware(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(statusMock).toHaveBeenCalledWith(403); + expect(jsonMock).toHaveBeenCalledWith({ message: 'Forbidden' }); + }); + + it('returns 401 when no user is present', async () => { + mockReq.user = undefined; + + const middleware = requireCapability(SystemCapabilities.ACCESS_ADMIN); + await middleware(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(statusMock).toHaveBeenCalledWith(401); + expect(jsonMock).toHaveBeenCalledWith({ message: 'Authentication required' }); + }); + + it('returns 500 on unexpected error', async () => { + mockGetUserPrincipals.mockRejectedValue(new Error('DB down')); + + const middleware = requireCapability(SystemCapabilities.ACCESS_ADMIN); + await middleware(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(statusMock).toHaveBeenCalledWith(500); + expect(jsonMock).toHaveBeenCalledWith({ message: 'Internal Server Error' }); + }); + }); + + describe('hasConfigCapability', () => { + const adminUser = { id: 'user-123', role: 'ADMIN' }; + const delegatedUser = { id: 'user-789', role: 'MANAGER' }; + + it('returns true when user has broad manage:configs capability', async () => { + mockGetUserPrincipals.mockResolvedValue(adminPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValue(true); + + const result = await hasConfigCapability(adminUser, 'endpoints'); + + expect(result).toBe(true); + expect(mockHasCapabilityForPrincipals).toHaveBeenCalledWith( + expect.objectContaining({ capability: SystemCapabilities.MANAGE_CONFIGS }), + ); + }); + + it('falls back to section-specific capability when broad check fails', async () => { + mockGetUserPrincipals.mockResolvedValue(userPrincipals); + // First call (broad) returns false, second call (section) returns true + mockHasCapabilityForPrincipals.mockResolvedValueOnce(false).mockResolvedValueOnce(true); + + const result = await hasConfigCapability(delegatedUser, 'endpoints'); + + expect(result).toBe(true); + expect(mockHasCapabilityForPrincipals).toHaveBeenCalledTimes(2); + expect(mockHasCapabilityForPrincipals).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ capability: configCapability('endpoints') }), + ); + }); + + it('returns false when user has neither broad nor section capability', async () => { + mockGetUserPrincipals.mockResolvedValue(userPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValue(false); + + const result = await hasConfigCapability(delegatedUser, 'balance'); + + expect(result).toBe(false); + }); + + it('checks read:configs when verb is "read"', async () => { + mockGetUserPrincipals.mockResolvedValue(userPrincipals); + mockHasCapabilityForPrincipals.mockResolvedValueOnce(false).mockResolvedValueOnce(true); + + const result = await hasConfigCapability(delegatedUser, 'endpoints', 'read'); + + expect(result).toBe(true); + expect(mockHasCapabilityForPrincipals).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ capability: SystemCapabilities.READ_CONFIGS }), + ); + expect(mockHasCapabilityForPrincipals).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ capability: readConfigCapability('endpoints') }), + ); + }); + }); +}); diff --git a/packages/api/src/middleware/capabilities.ts b/packages/api/src/middleware/capabilities.ts new file mode 100644 index 0000000000..c06a90ac8e --- /dev/null +++ b/packages/api/src/middleware/capabilities.ts @@ -0,0 +1,188 @@ +import { isMainThread } from 'node:worker_threads'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { + logger, + configCapability, + SystemCapabilities, + readConfigCapability, +} from '@librechat/data-schemas'; +import type { PrincipalType } from 'librechat-data-provider'; +import type { SystemCapability, ConfigSection } from '@librechat/data-schemas'; +import type { NextFunction, Response } from 'express'; +import type { Types } from 'mongoose'; +import type { ServerRequest } from '~/types/http'; + +interface ResolvedPrincipal { + principalType: PrincipalType; + principalId?: string | Types.ObjectId; +} + +interface CapabilityDeps { + getUserPrincipals: (params: { userId: string; role: string }) => Promise; + hasCapabilityForPrincipals: (params: { + principals: ResolvedPrincipal[]; + capability: SystemCapability; + tenantId?: string; + }) => Promise; +} + +interface CapabilityUser { + id: string; + role: string; + tenantId?: string; +} + +interface CapabilityStore { + principals: Map; + results: Map; +} + +export type HasCapabilityFn = ( + user: CapabilityUser, + capability: SystemCapability, +) => Promise; + +export type RequireCapabilityFn = ( + capability: SystemCapability, +) => (req: ServerRequest, res: Response, next: NextFunction) => Promise; + +export type HasConfigCapabilityFn = ( + user: CapabilityUser, + section: ConfigSection, + verb?: 'manage' | 'read', +) => Promise; + +/** + * Per-request store for caching resolved principals and capability check results. + * When running inside an Express request (via `capabilityContextMiddleware`), + * duplicate `hasCapability` calls within the same request are served from + * the in-memory Map instead of hitting the database again. + * Outside a request context (background jobs, tests), the store is undefined + * and every check falls through to the database — correct behavior. + */ +export const capabilityStore = new AsyncLocalStorage(); + +export function capabilityContextMiddleware( + _req: ServerRequest, + _res: Response, + next: NextFunction, +): void { + if (!isMainThread) { + logger.error( + '[capabilityContextMiddleware] Mounted in a worker thread — ' + + 'ALS context will not propagate to the main thread or other workers. ' + + 'This middleware should only run in the main Express process.', + ); + } + capabilityStore.run({ principals: new Map(), results: new Map() }, next); +} + +/** + * Factory that creates `hasCapability` and `requireCapability` with injected + * database methods. Follows the same dependency-injection pattern as + * `generateCheckAccess`. + */ +export function generateCapabilityCheck(deps: CapabilityDeps): { + hasCapability: HasCapabilityFn; + requireCapability: RequireCapabilityFn; + hasConfigCapability: HasConfigCapabilityFn; +} { + const { getUserPrincipals, hasCapabilityForPrincipals } = deps; + + let workerWarned = false; + + async function hasCapability( + user: CapabilityUser, + capability: SystemCapability, + ): Promise { + if (!isMainThread && !workerWarned) { + workerWarned = true; + logger.warn( + '[hasCapability] Called from a worker thread — ALS context is unavailable. ' + + 'Capability checks will hit the database on every call (no per-request caching). ' + + 'If this is intentional, no action needed.', + ); + } + + const store = capabilityStore.getStore(); + + const resultKey = `${user.id}:${user.tenantId ?? ''}:${capability}`; + const cached = store?.results.get(resultKey); + if (cached !== undefined) { + return cached; + } + + const principalKey = `${user.id}:${user.role}:${user.tenantId ?? ''}`; + let principals: ResolvedPrincipal[]; + const cachedPrincipals = store?.principals.get(principalKey); + if (cachedPrincipals) { + principals = cachedPrincipals; + } else { + principals = await getUserPrincipals({ userId: user.id, role: user.role }); + store?.principals.set(principalKey, principals); + } + + const result = await hasCapabilityForPrincipals({ + principals, + capability, + tenantId: user.tenantId, + }); + store?.results.set(resultKey, result); + return result; + } + + /** + * Checks if a user can manage or read a specific config section. + * First checks the broad capability (manage:configs / read:configs), + * then falls back to the section-specific capability (manage:configs:
). + */ + async function hasConfigCapability( + user: CapabilityUser, + section: ConfigSection, + verb: 'manage' | 'read' = 'manage', + ): Promise { + const broadCap = + verb === 'manage' ? SystemCapabilities.MANAGE_CONFIGS : SystemCapabilities.READ_CONFIGS; + if (await hasCapability(user, broadCap)) { + return true; + } + const sectionCap = + verb === 'manage' ? configCapability(section) : readConfigCapability(section); + return hasCapability(user, sectionCap); + } + + function requireCapability(capability: SystemCapability) { + return async (req: ServerRequest, res: Response, next: NextFunction) => { + try { + if (!req.user) { + res.status(401).json({ message: 'Authentication required' }); + return; + } + + const id = req.user.id ?? req.user._id?.toString(); + if (!id) { + res.status(401).json({ message: 'Authentication required' }); + return; + } + + const user: CapabilityUser = { + id, + role: req.user.role ?? '', + tenantId: (req.user as CapabilityUser).tenantId, + }; + + if (await hasCapability(user, capability)) { + next(); + return; + } + + res.status(403).json({ message: 'Forbidden' }); + } catch (err) { + logger.error(`[requireCapability] Error checking capability: ${capability}`, err); + res.status(500).json({ message: 'Internal Server Error' }); + } + }; + } + + return { hasCapability, requireCapability, hasConfigCapability }; +} diff --git a/packages/api/src/middleware/index.ts b/packages/api/src/middleware/index.ts index 7787d89dfe..a56b8e4a3e 100644 --- a/packages/api/src/middleware/index.ts +++ b/packages/api/src/middleware/index.ts @@ -4,5 +4,6 @@ export * from './error'; export * from './notFound'; export * from './balance'; export * from './json'; +export * from './capabilities'; export * from './concurrency'; export * from './checkBalance'; diff --git a/packages/data-schemas/src/index.ts b/packages/data-schemas/src/index.ts index ae69fc58bb..3a34b574ae 100644 --- a/packages/data-schemas/src/index.ts +++ b/packages/data-schemas/src/index.ts @@ -1,4 +1,5 @@ export * from './app'; +export * from './systemCapabilities'; export * from './common'; export * from './crypto'; export * from './schema'; diff --git a/packages/data-schemas/src/methods/aclEntry.spec.ts b/packages/data-schemas/src/methods/aclEntry.spec.ts index b6643c416e..df59268db4 100644 --- a/packages/data-schemas/src/methods/aclEntry.spec.ts +++ b/packages/data-schemas/src/methods/aclEntry.spec.ts @@ -959,4 +959,285 @@ describe('AclEntry Model Tests', () => { expect(permissionsMap.get(resource2.toString())).toBe(PermissionBits.EDIT); }); }); + + describe('deleteAclEntries', () => { + test('should delete entries matching the filter', async () => { + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.MCPSERVER, + resourceId, + PermissionBits.EDIT, + grantedById, + ); + + const result = await methods.deleteAclEntries({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.AGENT, + }); + + expect(result.deletedCount).toBe(1); + const remaining = await AclEntry.countDocuments({ principalId: userId }); + expect(remaining).toBe(1); + }); + + test('should delete all entries when filter matches multiple', async () => { + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + new mongoose.Types.ObjectId(), + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + new mongoose.Types.ObjectId(), + PermissionBits.EDIT, + grantedById, + ); + + const result = await methods.deleteAclEntries({ + principalType: PrincipalType.USER, + principalId: userId, + }); + + expect(result.deletedCount).toBe(2); + }); + + test('should return zero deletedCount when no match', async () => { + const result = await methods.deleteAclEntries({ + principalId: new mongoose.Types.ObjectId(), + }); + expect(result.deletedCount).toBe(0); + }); + }); + + describe('bulkWriteAclEntries', () => { + test('should perform bulk inserts', async () => { + const res1 = new mongoose.Types.ObjectId(); + const res2 = new mongoose.Types.ObjectId(); + + const result = await methods.bulkWriteAclEntries([ + { + insertOne: { + document: { + principalType: PrincipalType.USER, + principalId: userId, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: res1, + permBits: PermissionBits.VIEW, + grantedBy: grantedById, + grantedAt: new Date(), + }, + }, + }, + { + insertOne: { + document: { + principalType: PrincipalType.USER, + principalId: userId, + principalModel: PrincipalModel.USER, + resourceType: ResourceType.AGENT, + resourceId: res2, + permBits: PermissionBits.EDIT, + grantedBy: grantedById, + grantedAt: new Date(), + }, + }, + }, + ]); + + expect(result.insertedCount).toBe(2); + const entries = await AclEntry.countDocuments({ principalId: userId }); + expect(entries).toBe(2); + }); + + test('should perform bulk updates', async () => { + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + + await methods.bulkWriteAclEntries([ + { + updateOne: { + filter: { + principalType: PrincipalType.USER, + principalId: userId, + resourceId, + }, + update: { $set: { permBits: PermissionBits.VIEW | PermissionBits.EDIT } }, + }, + }, + ]); + + const entry = await AclEntry.findOne({ principalId: userId, resourceId }).lean(); + expect(entry?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); + }); + }); + + describe('findPublicResourceIds', () => { + test('should find resources with public VIEW access', async () => { + const publicRes1 = new mongoose.Types.ObjectId(); + const publicRes2 = new mongoose.Types.ObjectId(); + const privateRes = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.AGENT, + publicRes1, + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.AGENT, + publicRes2, + PermissionBits.VIEW | PermissionBits.EDIT, + grantedById, + ); + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + privateRes, + PermissionBits.VIEW, + grantedById, + ); + + const publicIds = await methods.findPublicResourceIds( + ResourceType.AGENT, + PermissionBits.VIEW, + ); + + expect(publicIds).toHaveLength(2); + const idStrings = publicIds.map((id) => id.toString()).sort(); + expect(idStrings).toEqual([publicRes1.toString(), publicRes2.toString()].sort()); + }); + + test('should filter by required permission bits', async () => { + const viewOnly = new mongoose.Types.ObjectId(); + const viewEdit = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.AGENT, + viewOnly, + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.AGENT, + viewEdit, + PermissionBits.VIEW | PermissionBits.EDIT, + grantedById, + ); + + const editableIds = await methods.findPublicResourceIds( + ResourceType.AGENT, + PermissionBits.EDIT, + ); + + expect(editableIds).toHaveLength(1); + expect(editableIds[0].toString()).toBe(viewEdit.toString()); + }); + + test('should return empty array when no public resources exist', async () => { + const ids = await methods.findPublicResourceIds(ResourceType.AGENT, PermissionBits.VIEW); + expect(ids).toEqual([]); + }); + + test('should filter by resource type', async () => { + const agentRes = new mongoose.Types.ObjectId(); + const mcpRes = new mongoose.Types.ObjectId(); + + await methods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.AGENT, + agentRes, + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.PUBLIC, + null, + ResourceType.MCPSERVER, + mcpRes, + PermissionBits.VIEW, + grantedById, + ); + + const agentIds = await methods.findPublicResourceIds(ResourceType.AGENT, PermissionBits.VIEW); + expect(agentIds).toHaveLength(1); + expect(agentIds[0].toString()).toBe(agentRes.toString()); + }); + }); + + describe('aggregateAclEntries', () => { + test('should run an aggregation pipeline and return results', async () => { + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.AGENT, + resourceId, + PermissionBits.VIEW, + grantedById, + ); + await methods.grantPermission( + PrincipalType.GROUP, + groupId, + ResourceType.AGENT, + resourceId, + PermissionBits.EDIT, + grantedById, + ); + await methods.grantPermission( + PrincipalType.USER, + userId, + ResourceType.MCPSERVER, + new mongoose.Types.ObjectId(), + PermissionBits.VIEW, + grantedById, + ); + + const results = await methods.aggregateAclEntries([ + { $group: { _id: '$resourceType', count: { $sum: 1 } } }, + { $sort: { _id: 1 } }, + ]); + + expect(results).toHaveLength(2); + const agentResult = results.find((r: { _id: string }) => r._id === ResourceType.AGENT); + expect(agentResult.count).toBe(2); + }); + + test('should return empty array for non-matching pipeline', async () => { + const results = await methods.aggregateAclEntries([ + { $match: { principalType: 'nonexistent' } }, + ]); + expect(results).toEqual([]); + }); + }); }); diff --git a/packages/data-schemas/src/methods/index.ts b/packages/data-schemas/src/methods/index.ts index 4192314b0b..11f00e7827 100644 --- a/packages/data-schemas/src/methods/index.ts +++ b/packages/data-schemas/src/methods/index.ts @@ -20,6 +20,7 @@ import { createPluginAuthMethods, type PluginAuthMethods } from './pluginAuth'; import { createAccessRoleMethods, type AccessRoleMethods } from './accessRole'; import { createUserGroupMethods, type UserGroupMethods } from './userGroup'; import { createAclEntryMethods, type AclEntryMethods } from './aclEntry'; +import { createSystemGrantMethods, type SystemGrantMethods } from './systemGrant'; import { createShareMethods, type ShareMethods } from './share'; /* Tier 1 — Simple CRUD */ import { createActionMethods, type ActionMethods } from './action'; @@ -62,6 +63,7 @@ export type AllMethods = UserMethods & MCPServerMethods & UserGroupMethods & AclEntryMethods & + SystemGrantMethods & ShareMethods & AccessRoleMethods & PluginAuthMethods & @@ -133,6 +135,8 @@ export function createMethods( // ACL entry methods (used internally for removeAllPermissions) const aclEntryMethods = createAclEntryMethods(mongoose); + const systemGrantMethods = createSystemGrantMethods(mongoose); + // Internal removeAllPermissions: use deleteAclEntries from aclEntryMethods // instead of requiring it as an external dep from PermissionService const removeAllPermissions = @@ -176,6 +180,7 @@ export function createMethods( ...createAccessRoleMethods(mongoose), ...createUserGroupMethods(mongoose), ...aclEntryMethods, + ...systemGrantMethods, ...createShareMethods(mongoose), ...createPluginAuthMethods(mongoose), /* Tier 1 */ @@ -212,6 +217,7 @@ export type { MCPServerMethods, UserGroupMethods, AclEntryMethods, + SystemGrantMethods, ShareMethods, AccessRoleMethods, PluginAuthMethods, diff --git a/packages/data-schemas/src/methods/prompt.spec.ts b/packages/data-schemas/src/methods/prompt.spec.ts index 0a8c2c247e..6a02b8bc3b 100644 --- a/packages/data-schemas/src/methods/prompt.spec.ts +++ b/packages/data-schemas/src/methods/prompt.spec.ts @@ -582,8 +582,6 @@ describe('Prompt ACL Permissions', () => { await methods.deletePrompt({ promptId: testPromptId, groupId: testPromptGroup._id, - author: testUsers.owner._id, - role: SystemRoles.USER, }); // Verify ACL entries are removed diff --git a/packages/data-schemas/src/methods/prompt.ts b/packages/data-schemas/src/methods/prompt.ts index 1420495ac2..4edfc9f408 100644 --- a/packages/data-schemas/src/methods/prompt.ts +++ b/packages/data-schemas/src/methods/prompt.ts @@ -1,5 +1,5 @@ import type { Model, Types } from 'mongoose'; -import { SystemRoles, ResourceType, SystemCategories } from 'librechat-data-provider'; +import { ResourceType, SystemCategories } from 'librechat-data-provider'; import type { IPrompt, IPromptGroup, IPromptGroupDocument } from '~/types'; import { escapeRegExp } from '~/utils/string'; import logger from '~/config/winston'; @@ -150,27 +150,18 @@ export function createPromptMethods(mongoose: typeof import('mongoose'), deps: P /** * Delete a prompt group and its prompts, cleaning up ACL permissions. + * + * **Authorization is enforced upstream.** This method performs no ownership + * check — it deletes any group by ID. Callers must gate access via + * `canAccessPromptGroupResource` middleware before invoking this. */ - async function deletePromptGroup({ - _id, - author, - role, - }: { - _id: string; - author?: string; - role?: string; - }) { + async function deletePromptGroup({ _id }: { _id: string }) { const PromptGroup = mongoose.models.PromptGroup as Model; const Prompt = mongoose.models.Prompt as Model; const query: Record = { _id }; const groupQuery: Record = { groupId: new ObjectId(_id) }; - if (author && role !== SystemRoles.ADMIN) { - query.author = author; - groupQuery.author = author; - } - const response = await PromptGroup.deleteOne(query); if (!response || response.deletedCount === 0) { @@ -478,25 +469,22 @@ export function createPromptMethods(mongoose: typeof import('mongoose'), deps: P /** * Delete a prompt, potentially removing the group if it's the last prompt. + * + * **Authorization is enforced upstream.** This method performs no ownership + * check — it deletes any prompt by ID. Callers must gate access via + * `canAccessPromptViaGroup` middleware before invoking this. */ async function deletePrompt({ promptId, groupId, - author, - role, }: { promptId: string | Types.ObjectId; groupId: string | Types.ObjectId; - author: string | Types.ObjectId; - role?: string; }) { const Prompt = mongoose.models.Prompt as Model; const PromptGroup = mongoose.models.PromptGroup as Model; - const query: Record = { _id: promptId, groupId, author }; - if (role === SystemRoles.ADMIN) { - delete query.author; - } + const query: Record = { _id: promptId, groupId }; const { deletedCount } = await Prompt.deleteOne(query); if (deletedCount === 0) { throw new Error('Failed to delete the prompt'); diff --git a/packages/data-schemas/src/methods/systemGrant.spec.ts b/packages/data-schemas/src/methods/systemGrant.spec.ts new file mode 100644 index 0000000000..fb886c74d3 --- /dev/null +++ b/packages/data-schemas/src/methods/systemGrant.spec.ts @@ -0,0 +1,840 @@ +import mongoose, { Types } from 'mongoose'; +import { PrincipalType, SystemRoles } from 'librechat-data-provider'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import type * as t from '~/types'; +import type { SystemCapability } from '~/systemCapabilities'; +import { SystemCapabilities, CapabilityImplications } from '~/systemCapabilities'; +import { createSystemGrantMethods } from './systemGrant'; +import systemGrantSchema from '~/schema/systemGrant'; +import logger from '~/config/winston'; + +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +})); + +let mongoServer: MongoMemoryServer; +let SystemGrant: mongoose.Model; +let methods: ReturnType; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + SystemGrant = + mongoose.models.SystemGrant || mongoose.model('SystemGrant', systemGrantSchema); + methods = createSystemGrantMethods(mongoose); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await SystemGrant.deleteMany({}); +}); + +describe('systemGrant methods', () => { + describe('seedSystemGrants', () => { + it('seeds every SystemCapabilities value for the ADMIN role', async () => { + await methods.seedSystemGrants(); + + const grants = await SystemGrant.find({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + }).lean(); + + const expected = Object.values(SystemCapabilities).sort(); + const actual = grants.map((g) => g.capability).sort(); + expect(actual).toEqual(expected); + }); + + it('is idempotent — duplicate calls produce no extra documents', async () => { + await methods.seedSystemGrants(); + await methods.seedSystemGrants(); + await methods.seedSystemGrants(); + + const count = await SystemGrant.countDocuments({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + }); + expect(count).toBe(Object.values(SystemCapabilities).length); + }); + + it('seeds platform-level grants (no tenantId field)', async () => { + await methods.seedSystemGrants(); + + const withTenant = await SystemGrant.countDocuments({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + tenantId: { $exists: true }, + }); + expect(withTenant).toBe(0); + }); + + it('does not throw when called (try-catch protects startup)', async () => { + await expect(methods.seedSystemGrants()).resolves.not.toThrow(); + }); + + it('retries on transient failure and succeeds', async () => { + jest.useFakeTimers(); + jest.spyOn(SystemGrant, 'bulkWrite').mockRejectedValueOnce(new Error('disk full')); + + const seedPromise = methods.seedSystemGrants(); + await jest.advanceTimersByTimeAsync(5000); + await seedPromise; + + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Attempt 1/3 failed')); + jest.useRealTimers(); + }); + + it('logs error after all retries exhausted', async () => { + jest.useFakeTimers(); + jest + .spyOn(SystemGrant, 'bulkWrite') + .mockRejectedValueOnce(new Error('disk full')) + .mockRejectedValueOnce(new Error('disk full')) + .mockRejectedValueOnce(new Error('disk full')); + + const seedPromise = methods.seedSystemGrants(); + await jest.advanceTimersByTimeAsync(10000); + await seedPromise; + + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to seed capabilities after all retries'), + expect.any(Error), + ); + jest.useRealTimers(); + }); + }); + + describe('grantCapability', () => { + it('creates a grant and returns the document', async () => { + const userId = new Types.ObjectId(); + const doc = await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + expect(doc).toBeTruthy(); + expect(doc!.principalType).toBe(PrincipalType.USER); + expect(doc!.capability).toBe(SystemCapabilities.READ_USERS); + expect(doc!.grantedAt).toBeInstanceOf(Date); + }); + + it('is idempotent — second call does not create a duplicate', async () => { + const userId = new Types.ObjectId(); + const params = { + principalType: PrincipalType.USER as const, + principalId: userId, + capability: SystemCapabilities.READ_USERS as SystemCapability, + }; + + await methods.grantCapability(params); + await methods.grantCapability(params); + + const count = await SystemGrant.countDocuments({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + expect(count).toBe(1); + }); + + it('stores grantedBy when provided', async () => { + const userId = new Types.ObjectId(); + const grantedBy = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + grantedBy, + }); + + const grant = await SystemGrant.findOne({ + principalType: PrincipalType.USER, + principalId: userId, + }).lean(); + + expect(grant!.grantedBy!.toString()).toBe(grantedBy.toString()); + }); + + it('stores tenant-scoped grants with tenantId field present', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USAGE, + tenantId: 'tenant-abc', + }); + + const grant = await SystemGrant.findOne({ + principalType: PrincipalType.USER, + principalId: userId, + tenantId: 'tenant-abc', + }).lean(); + + expect(grant).toBeTruthy(); + expect(grant!.tenantId).toBe('tenant-abc'); + }); + + it('normalizes string userId to ObjectId for USER principal', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId.toString(), + capability: SystemCapabilities.READ_USERS, + }); + + const grant = await SystemGrant.findOne({ capability: SystemCapabilities.READ_USERS }).lean(); + expect(grant!.principalId.toString()).toBe(userId.toString()); + expect(grant!.principalId).toBeInstanceOf(Types.ObjectId); + }); + + it('normalizes string groupId to ObjectId for GROUP principal', async () => { + const groupId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.GROUP, + principalId: groupId.toString(), + capability: SystemCapabilities.READ_AGENTS, + }); + + const grant = await SystemGrant.findOne({ + capability: SystemCapabilities.READ_AGENTS, + }).lean(); + expect(grant!.principalId).toBeInstanceOf(Types.ObjectId); + }); + + it('keeps ROLE principalId as a string (no ObjectId cast)', async () => { + await methods.grantCapability({ + principalType: PrincipalType.ROLE, + principalId: 'CUSTOM_ROLE', + capability: SystemCapabilities.READ_CONFIGS, + }); + + const grant = await SystemGrant.findOne({ + principalType: PrincipalType.ROLE, + principalId: 'CUSTOM_ROLE', + }).lean(); + + expect(grant).toBeTruthy(); + expect(typeof grant!.principalId).toBe('string'); + }); + + it('allows same capability for same principal in different tenants', async () => { + const userId = new Types.ObjectId(); + const params = { + principalType: PrincipalType.USER as const, + principalId: userId, + capability: SystemCapabilities.ACCESS_ADMIN as SystemCapability, + }; + + await methods.grantCapability({ ...params, tenantId: 'tenant-1' }); + await methods.grantCapability({ ...params, tenantId: 'tenant-2' }); + + const count = await SystemGrant.countDocuments({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.ACCESS_ADMIN, + }); + expect(count).toBe(2); + }); + + it('handles E11000 race condition — returns existing doc instead of throwing', async () => { + const userId = new Types.ObjectId(); + const params = { + principalType: PrincipalType.USER as const, + principalId: userId, + capability: SystemCapabilities.READ_USERS as SystemCapability, + }; + + const original = await methods.grantCapability(params); + + // Simulate a race: findOneAndUpdate upserts but hits a duplicate key + const model = mongoose.models.SystemGrant; + jest + .spyOn(model, 'findOneAndUpdate') + .mockRejectedValueOnce( + Object.assign(new Error('E11000 duplicate key error'), { code: 11000 }), + ); + + const result = await methods.grantCapability(params); + expect(result).toBeTruthy(); + expect(result!.capability).toBe(SystemCapabilities.READ_USERS); + expect(result!.principalId.toString()).toBe(original!.principalId.toString()); + }); + + it('re-throws non-E11000 errors from findOneAndUpdate', async () => { + const model = mongoose.models.SystemGrant; + jest.spyOn(model, 'findOneAndUpdate').mockRejectedValueOnce(new Error('connection timeout')); + + await expect( + methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: SystemCapabilities.READ_USERS, + }), + ).rejects.toThrow('connection timeout'); + }); + + it('throws TypeError for invalid ObjectId string on USER principal', async () => { + await expect( + methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: 'not-a-valid-objectid', + capability: SystemCapabilities.READ_USERS, + }), + ).rejects.toThrow(TypeError); + }); + + it('throws TypeError for invalid ObjectId string on GROUP principal', async () => { + await expect( + methods.grantCapability({ + principalType: PrincipalType.GROUP, + principalId: 'also-invalid', + capability: SystemCapabilities.READ_AGENTS, + }), + ).rejects.toThrow(TypeError); + }); + + it('accepts any string for ROLE principal without ObjectId validation', async () => { + const doc = await methods.grantCapability({ + principalType: PrincipalType.ROLE, + principalId: 'ANY_STRING_HERE', + capability: SystemCapabilities.READ_CONFIGS, + }); + expect(doc).toBeTruthy(); + expect(doc!.principalId).toBe('ANY_STRING_HERE'); + }); + }); + + describe('revokeCapability', () => { + it('removes the grant document', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + await methods.revokeCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USERS, + }); + + const grant = await SystemGrant.findOne({ + principalType: PrincipalType.USER, + principalId: userId, + }).lean(); + expect(grant).toBeNull(); + }); + + it('is a no-op when the grant does not exist', async () => { + await expect( + methods.revokeCapability({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: SystemCapabilities.MANAGE_USERS, + }), + ).resolves.not.toThrow(); + }); + + it('normalizes string userId when revoking', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USAGE, + }); + + await methods.revokeCapability({ + principalType: PrincipalType.USER, + principalId: userId.toString(), + capability: SystemCapabilities.READ_USAGE, + }); + + const count = await SystemGrant.countDocuments({ + principalType: PrincipalType.USER, + principalId: userId, + }); + expect(count).toBe(0); + }); + + it('only revokes the specified tenant grant', async () => { + const userId = new Types.ObjectId(); + const params = { + principalType: PrincipalType.USER as const, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS as SystemCapability, + }; + + await methods.grantCapability({ ...params, tenantId: 'tenant-1' }); + await methods.grantCapability({ ...params, tenantId: 'tenant-2' }); + + await methods.revokeCapability({ ...params, tenantId: 'tenant-1' }); + + const remaining = await SystemGrant.find({ + principalType: PrincipalType.USER, + principalId: userId, + }).lean(); + + expect(remaining).toHaveLength(1); + expect(remaining[0].tenantId).toBe('tenant-2'); + }); + + it('throws TypeError for invalid ObjectId string on USER principal', async () => { + await expect( + methods.revokeCapability({ + principalType: PrincipalType.USER, + principalId: 'bad-id', + capability: SystemCapabilities.READ_USERS, + }), + ).rejects.toThrow(TypeError); + }); + }); + + describe('hasCapabilityForPrincipals', () => { + it('returns true when a role principal holds the capability', async () => { + await methods.seedSystemGrants(); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [ + { principalType: PrincipalType.USER, principalId: new Types.ObjectId() }, + { principalType: PrincipalType.ROLE, principalId: SystemRoles.ADMIN }, + { principalType: PrincipalType.PUBLIC }, + ], + capability: SystemCapabilities.ACCESS_ADMIN, + }); + expect(result).toBe(true); + }); + + it('returns false when no principal has the capability', async () => { + const result = await methods.hasCapabilityForPrincipals({ + principals: [ + { principalType: PrincipalType.USER, principalId: new Types.ObjectId() }, + { principalType: PrincipalType.ROLE, principalId: SystemRoles.USER }, + { principalType: PrincipalType.PUBLIC }, + ], + capability: SystemCapabilities.ACCESS_ADMIN, + }); + expect(result).toBe(false); + }); + + it('returns false for an empty principals array', async () => { + const result = await methods.hasCapabilityForPrincipals({ + principals: [], + capability: SystemCapabilities.ACCESS_ADMIN, + }); + expect(result).toBe(false); + }); + + it('returns false when only PUBLIC principals are present', async () => { + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.PUBLIC }], + capability: SystemCapabilities.ACCESS_ADMIN, + }); + expect(result).toBe(false); + }); + + it('matches user-level grants', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [ + { principalType: PrincipalType.USER, principalId: userId }, + { principalType: PrincipalType.ROLE, principalId: SystemRoles.USER }, + ], + capability: SystemCapabilities.READ_CONFIGS, + }); + expect(result).toBe(true); + }); + + it('matches group-level grants', async () => { + const groupId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.GROUP, + principalId: groupId, + capability: SystemCapabilities.READ_USAGE, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [ + { principalType: PrincipalType.USER, principalId: new Types.ObjectId() }, + { principalType: PrincipalType.GROUP, principalId: groupId }, + ], + capability: SystemCapabilities.READ_USAGE, + }); + expect(result).toBe(true); + }); + + it('finds grant when string userId was used to create it and ObjectId to query', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId.toString(), + capability: SystemCapabilities.READ_USAGE, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_USAGE, + }); + expect(result).toBe(true); + }); + + describe('capability implications', () => { + it.each( + ( + Object.entries(CapabilityImplications) as [SystemCapability, SystemCapability[]][] + ).flatMap(([broad, implied]) => implied.map((imp) => [broad, imp] as const)), + )('%s implies %s', async (broadCap, impliedCap) => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: broadCap, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: impliedCap, + }); + expect(result).toBe(true); + }); + + it.each( + ( + Object.entries(CapabilityImplications) as [SystemCapability, SystemCapability[]][] + ).flatMap(([broad, implied]) => implied.map((imp) => [imp, broad] as const)), + )('%s does NOT imply %s (reverse)', async (narrowCap, broadCap) => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: narrowCap, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: broadCap, + }); + expect(result).toBe(false); + }); + }); + + describe('tenant scoping', () => { + it('tenant-scoped grant does not match platform-level query', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_CONFIGS, + }); + expect(result).toBe(false); + }); + + it('platform-level grant does not match tenant-scoped query', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + expect(result).toBe(false); + }); + + it('tenant-scoped grant matches same-tenant query', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + expect(result).toBe(true); + }); + + it('tenant-scoped grant does not match different tenant', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + + const result = await methods.hasCapabilityForPrincipals({ + principals: [{ principalType: PrincipalType.USER, principalId: userId }], + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-2', + }); + expect(result).toBe(false); + }); + }); + }); + + describe('getCapabilitiesForPrincipal', () => { + it('lists all capabilities for the ADMIN role after seeding', async () => { + await methods.seedSystemGrants(); + + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + }); + + expect(grants).toHaveLength(Object.values(SystemCapabilities).length); + const caps = grants.map((g) => g.capability).sort(); + expect(caps).toEqual(Object.values(SystemCapabilities).sort()); + }); + + it('returns empty array when principal has no grants', async () => { + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.USER, + }); + expect(grants).toHaveLength(0); + }); + + it('normalizes string userId for lookup', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USAGE, + }); + + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.USER, + principalId: userId.toString(), + }); + expect(grants).toHaveLength(1); + expect(grants[0].capability).toBe(SystemCapabilities.READ_USAGE); + }); + + it('only returns grants for the specified tenant', async () => { + const userId = new Types.ObjectId(); + + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_CONFIGS, + tenantId: 'tenant-1', + }); + await methods.grantCapability({ + principalType: PrincipalType.USER, + principalId: userId, + capability: SystemCapabilities.READ_USAGE, + tenantId: 'tenant-2', + }); + + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.USER, + principalId: userId, + tenantId: 'tenant-1', + }); + expect(grants).toHaveLength(1); + expect(grants[0].capability).toBe(SystemCapabilities.READ_CONFIGS); + }); + + it('throws TypeError for invalid ObjectId string on USER principal', async () => { + await expect( + methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.USER, + principalId: 'not-valid', + }), + ).rejects.toThrow(TypeError); + }); + }); + + describe('schema validation', () => { + it('rejects null tenantId at the schema level', async () => { + await expect( + SystemGrant.create({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: SystemCapabilities.READ_USERS, + tenantId: null, + }), + ).rejects.toThrow(/tenantId/); + }); + + it('rejects empty string tenantId at the schema level', async () => { + await expect( + SystemGrant.create({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: SystemCapabilities.READ_USERS, + tenantId: '', + }), + ).rejects.toThrow(/tenantId/); + }); + + it('rejects invalid principalType values', async () => { + await expect( + SystemGrant.create({ + principalType: 'INVALID_TYPE', + principalId: new Types.ObjectId(), + capability: SystemCapabilities.READ_USERS, + }), + ).rejects.toThrow(/principalType/); + }); + + it('requires principalType field', async () => { + await expect( + SystemGrant.create({ + principalId: new Types.ObjectId(), + capability: SystemCapabilities.READ_USERS, + }), + ).rejects.toThrow(/principalType/); + }); + + it('requires principalId field', async () => { + await expect( + SystemGrant.create({ + principalType: PrincipalType.USER, + capability: SystemCapabilities.READ_USERS, + }), + ).rejects.toThrow(/principalId/); + }); + + it('requires capability field', async () => { + await expect( + SystemGrant.create({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + }), + ).rejects.toThrow(/capability/); + }); + + it('rejects invalid capability strings', async () => { + await expect( + SystemGrant.create({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: 'god:mode', + }), + ).rejects.toThrow(/Invalid capability string/); + }); + + it('accepts valid section-level config capabilities', async () => { + const doc = await SystemGrant.create({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: 'manage:configs:endpoints', + }); + expect(doc.capability).toBe('manage:configs:endpoints'); + }); + + it('accepts valid assign config capabilities', async () => { + const doc = await SystemGrant.create({ + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: 'assign:configs:group', + }); + expect(doc.capability).toBe('assign:configs:group'); + }); + + it('enforces unique compound index (principalType + principalId + capability + tenantId)', async () => { + const doc = { + principalType: PrincipalType.USER, + principalId: new Types.ObjectId(), + capability: SystemCapabilities.READ_USERS, + }; + + await SystemGrant.create(doc); + + await expect(SystemGrant.create(doc)).rejects.toThrow(/duplicate key|E11000/); + }); + + it('rejects duplicate platform-level grants (absent tenantId) — non-sparse index', async () => { + const principalId = new Types.ObjectId(); + + await SystemGrant.create({ + principalType: PrincipalType.USER, + principalId, + capability: SystemCapabilities.ACCESS_ADMIN, + }); + + await expect( + SystemGrant.create({ + principalType: PrincipalType.USER, + principalId, + capability: SystemCapabilities.ACCESS_ADMIN, + }), + ).rejects.toThrow(/duplicate key|E11000/); + }); + + it('allows same grant for different tenants (tenantId is part of unique key)', async () => { + const principalId = new Types.ObjectId(); + const base = { + principalType: PrincipalType.USER, + principalId, + capability: SystemCapabilities.ACCESS_ADMIN, + }; + + await SystemGrant.create({ ...base, tenantId: 'tenant-a' }); + await SystemGrant.create({ ...base, tenantId: 'tenant-b' }); + + const count = await SystemGrant.countDocuments({ principalId }); + expect(count).toBe(2); + }); + + it('platform-level and tenant-scoped grants coexist (different unique key values)', async () => { + const principalId = new Types.ObjectId(); + const base = { + principalType: PrincipalType.USER, + principalId, + capability: SystemCapabilities.ACCESS_ADMIN, + }; + + await SystemGrant.create(base); + await SystemGrant.create({ ...base, tenantId: 'tenant-1' }); + + const count = await SystemGrant.countDocuments({ principalId }); + expect(count).toBe(2); + }); + }); +}); diff --git a/packages/data-schemas/src/methods/systemGrant.ts b/packages/data-schemas/src/methods/systemGrant.ts new file mode 100644 index 0000000000..f45d9fde9d --- /dev/null +++ b/packages/data-schemas/src/methods/systemGrant.ts @@ -0,0 +1,266 @@ +import { PrincipalType, SystemRoles } from 'librechat-data-provider'; +import type { Types, Model, ClientSession } from 'mongoose'; +import type { SystemCapability } from '~/systemCapabilities'; +import type { ISystemGrant } from '~/types'; +import { SystemCapabilities, CapabilityImplications } from '~/systemCapabilities'; +import { normalizePrincipalId } from '~/utils/principal'; +import logger from '~/config/winston'; + +/** + * Precomputed reverse map: for each capability, which broader capabilities imply it. + * Built once at module load so `hasCapabilityForPrincipals` avoids O(N×M) per call. + */ +type BaseSystemCapability = (typeof SystemCapabilities)[keyof typeof SystemCapabilities]; +const reverseImplications: Partial> = {}; +for (const [broad, implied] of Object.entries(CapabilityImplications)) { + for (const cap of implied as BaseSystemCapability[]) { + (reverseImplications[cap] ??= []).push(broad as BaseSystemCapability); + } +} + +export function createSystemGrantMethods(mongoose: typeof import('mongoose')) { + /** + * Check if any of the given principals holds a specific capability. + * Follows the same principal-resolution pattern as AclEntry: + * getUserPrincipals → $or query. + * + * @param principals - Resolved principal list from getUserPrincipals + * @param capability - The capability to check + * @param tenantId - If present, checks tenant-scoped grant; if absent, checks platform-level + */ + async function hasCapabilityForPrincipals({ + principals, + capability, + tenantId, + }: { + principals: Array<{ principalType: PrincipalType; principalId?: string | Types.ObjectId }>; + capability: SystemCapability; + tenantId?: string; + }): Promise { + const SystemGrant = mongoose.models.SystemGrant as Model; + const principalsQuery = principals + .filter((p) => p.principalType !== PrincipalType.PUBLIC) + .map((p) => ({ principalType: p.principalType, principalId: p.principalId })); + + if (!principalsQuery.length) { + return false; + } + + const impliedBy = reverseImplications[capability as keyof typeof reverseImplications] ?? []; + const capabilityQuery = impliedBy.length ? { $in: [capability, ...impliedBy] } : capability; + + const query: Record = { + $or: principalsQuery, + capability: capabilityQuery, + }; + + /* + * TODO(#12091): In multi-tenant mode, platform-level grants (tenantId absent) + * should also satisfy tenant-scoped checks so that seeded ADMIN grants remain + * effective. When tenantId is set, query both tenant-scoped AND platform-level: + * query.$or = [{ tenantId }, { tenantId: { $exists: false } }] + * Also: getUserPrincipals currently has no tenantId param, so group memberships + * are returned across all tenants. Filter by tenant there too. + */ + if (tenantId != null) { + query.tenantId = tenantId; + } else { + query.tenantId = { $exists: false }; + } + + const doc = await SystemGrant.exists(query); + return doc != null; + } + + /** + * Grant a capability to a principal. Upsert — idempotent. + */ + async function grantCapability( + { + principalType, + principalId, + capability, + tenantId, + grantedBy, + }: { + principalType: PrincipalType; + principalId: string | Types.ObjectId; + capability: SystemCapability; + tenantId?: string; + grantedBy?: string | Types.ObjectId; + }, + session?: ClientSession, + ): Promise { + const SystemGrant = mongoose.models.SystemGrant as Model; + + const normalizedPrincipalId = normalizePrincipalId(principalId, principalType); + + const filter: Record = { + principalType, + principalId: normalizedPrincipalId, + capability, + }; + + if (tenantId != null) { + filter.tenantId = tenantId; + } else { + filter.tenantId = { $exists: false }; + } + + const update = { + $set: { + grantedAt: new Date(), + ...(grantedBy != null && { grantedBy }), + }, + $setOnInsert: { + principalType, + principalId: normalizedPrincipalId, + capability, + ...(tenantId != null && { tenantId }), + }, + }; + + const options = { + upsert: true, + new: true, + ...(session ? { session } : {}), + }; + + try { + return await SystemGrant.findOneAndUpdate(filter, update, options); + } catch (err) { + if ((err as { code?: number }).code === 11000) { + return (await SystemGrant.findOne(filter).lean()) as ISystemGrant | null; + } + throw err; + } + } + + /** + * Revoke a capability from a principal. + */ + async function revokeCapability( + { + principalType, + principalId, + capability, + tenantId, + }: { + principalType: PrincipalType; + principalId: string | Types.ObjectId; + capability: SystemCapability; + tenantId?: string; + }, + session?: ClientSession, + ): Promise { + const SystemGrant = mongoose.models.SystemGrant as Model; + + const normalizedPrincipalId = normalizePrincipalId(principalId, principalType); + + const filter: Record = { + principalType, + principalId: normalizedPrincipalId, + capability, + }; + + if (tenantId != null) { + filter.tenantId = tenantId; + } else { + filter.tenantId = { $exists: false }; + } + + const options = session ? { session } : {}; + await SystemGrant.deleteOne(filter, options); + } + + /** + * List all capabilities held by a principal — used by the capabilities + * introspection endpoint. + */ + async function getCapabilitiesForPrincipal({ + principalType, + principalId, + tenantId, + }: { + principalType: PrincipalType; + principalId: string | Types.ObjectId; + tenantId?: string; + }): Promise { + const SystemGrant = mongoose.models.SystemGrant as Model; + + const filter: Record = { + principalType, + principalId: normalizePrincipalId(principalId, principalType), + }; + + if (tenantId != null) { + filter.tenantId = tenantId; + } else { + filter.tenantId = { $exists: false }; + } + + return await SystemGrant.find(filter).lean(); + } + + /** + * Seed the ADMIN role with all system capabilities (no tenantId — single-instance mode). + * Idempotent and concurrency-safe: uses bulkWrite with ordered:false so parallel + * server instances (K8s rolling deploy, PM2 cluster) do not race on E11000. + * Retries up to 3 times with exponential backoff on transient failures. + */ + async function seedSystemGrants(): Promise { + const maxRetries = 3; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const SystemGrant = mongoose.models.SystemGrant as Model; + const now = new Date(); + const ops = Object.values(SystemCapabilities).map((capability) => ({ + updateOne: { + filter: { + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + capability, + tenantId: { $exists: false }, + }, + update: { + $setOnInsert: { + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + capability, + grantedAt: now, + }, + }, + upsert: true, + }, + })); + await SystemGrant.bulkWrite(ops, { ordered: false }); + return; + } catch (err) { + if (attempt < maxRetries) { + const delay = 1000 * Math.pow(2, attempt - 1); + logger.warn( + `[seedSystemGrants] Attempt ${attempt}/${maxRetries} failed, retrying in ${delay}ms: ${(err as Error).message ?? String(err)}`, + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + } else { + logger.error( + '[seedSystemGrants] Failed to seed capabilities after all retries. ' + + 'Admin panel access requires these grants. Manual recovery: ' + + 'db.systemgrants.insertMany([...]) with ADMIN role grants for each capability.', + err, + ); + } + } + } + } + + return { + grantCapability, + seedSystemGrants, + revokeCapability, + hasCapabilityForPrincipals, + getCapabilitiesForPrincipal, + }; +} + +export type SystemGrantMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/userGroup.spec.ts b/packages/data-schemas/src/methods/userGroup.spec.ts index 9de8eaf912..675fdb2592 100644 --- a/packages/data-schemas/src/methods/userGroup.spec.ts +++ b/packages/data-schemas/src/methods/userGroup.spec.ts @@ -1,12 +1,12 @@ -import mongoose from 'mongoose'; -import { PrincipalType } from 'librechat-data-provider'; +import mongoose, { Types } from 'mongoose'; +import { PrincipalType, SystemRoles } from 'librechat-data-provider'; import { MongoMemoryServer } from 'mongodb-memory-server'; import type * as t from '~/types'; import { createUserGroupMethods } from './userGroup'; import groupSchema from '~/schema/group'; import userSchema from '~/schema/user'; +import roleSchema from '~/schema/role'; -/** Mocking logger */ jest.mock('~/config/winston', () => ({ error: jest.fn(), info: jest.fn(), @@ -16,15 +16,16 @@ jest.mock('~/config/winston', () => ({ let mongoServer: MongoMemoryServer; let Group: mongoose.Model; let User: mongoose.Model; +let Role: mongoose.Model; let methods: ReturnType; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); Group = mongoose.models.Group || mongoose.model('Group', groupSchema); User = mongoose.models.User || mongoose.model('User', userSchema); + Role = mongoose.models.Role || mongoose.model('Role', roleSchema); methods = createUserGroupMethods(mongoose); - await mongoose.connect(mongoUri); + await mongoose.connect(mongoServer.getUri()); }); afterAll(async () => { @@ -33,530 +34,775 @@ afterAll(async () => { }); beforeEach(async () => { - await mongoose.connection.dropDatabase(); + await Group.deleteMany({}); + await User.deleteMany({}); + await Role.deleteMany({}); }); -describe('User Group Methods Tests', () => { - describe('Group Query Methods', () => { - let testGroup: t.IGroup; - let testUser: t.IUser; +async function createTestUser(overrides: Partial = {}) { + return User.create({ + name: 'Test User', + email: `user-${new Types.ObjectId()}@test.com`, + password: 'password123', + provider: 'local', + role: SystemRoles.USER, + ...overrides, + }); +} - beforeEach(async () => { - /** Create a test user */ - testUser = await User.create({ - name: 'Test User', - email: 'test@example.com', - password: 'password123', - provider: 'local', - }); +describe('userGroup methods', () => { + describe('findGroupById', () => { + it('returns the group when it exists', async () => { + const group = await Group.create({ name: 'Engineering', source: 'local' }); + const found = await methods.findGroupById(group._id); + expect(found).toBeTruthy(); + expect(found!.name).toBe('Engineering'); + }); - /** Create a test group */ - testGroup = await Group.create({ - name: 'Test Group', + it('returns null when group does not exist', async () => { + const found = await methods.findGroupById(new Types.ObjectId()); + expect(found).toBeNull(); + }); + + it('respects projection parameter', async () => { + const group = await Group.create({ + name: 'Engineering', + description: 'The eng team', source: 'local', - memberIds: [(testUser._id as mongoose.Types.ObjectId).toString()], }); - - /** No need to add group to user - using one-way relationship via Group.memberIds */ + const found = await methods.findGroupById(group._id, { name: 1 }); + expect(found!.name).toBe('Engineering'); + expect(found!.description).toBeUndefined(); }); + }); - test('should find group by ID', async () => { - const group = await methods.findGroupById(testGroup._id as mongoose.Types.ObjectId); - - expect(group).toBeDefined(); - expect(group?._id.toString()).toBe(testGroup._id.toString()); - expect(group?.name).toBe(testGroup.name); - }); - - test('should find group by ID with specific projection', async () => { - const group = await methods.findGroupById(testGroup._id as mongoose.Types.ObjectId, { - name: 1, - }); - - expect(group).toBeDefined(); - expect(group?._id).toBeDefined(); - expect(group?.name).toBe(testGroup.name); - expect(group?.memberIds).toBeUndefined(); - }); - - test('should find group by external ID', async () => { - /** Create an external ID group first */ - const entraGroup = await Group.create({ + describe('findGroupByExternalId', () => { + it('finds a group by its external Entra ID', async () => { + await Group.create({ name: 'Entra Group', source: 'entra', - idOnTheSource: 'entra-id-12345', + idOnTheSource: 'entra-abc-123', }); - - const group = await methods.findGroupByExternalId('entra-id-12345', 'entra'); - - expect(group).toBeDefined(); - expect(group?._id.toString()).toBe(entraGroup._id.toString()); - expect(group?.idOnTheSource).toBe('entra-id-12345'); + const found = await methods.findGroupByExternalId('entra-abc-123', 'entra'); + expect(found).toBeTruthy(); + expect(found!.name).toBe('Entra Group'); }); - test('should return null for non-existent external ID', async () => { - const group = await methods.findGroupByExternalId('non-existent-id', 'entra'); - expect(group).toBeNull(); + it('returns null when no match', async () => { + const found = await methods.findGroupByExternalId('nonexistent', 'entra'); + expect(found).toBeNull(); + }); + }); + + describe('findGroupsByNamePattern', () => { + beforeEach(async () => { + await Group.create([ + { name: 'Engineering', source: 'local', description: 'Eng team' }, + { name: 'Design', source: 'local', email: 'design@co.com' }, + { name: 'Entra Eng', source: 'entra', idOnTheSource: 'ext-1' }, + ]); }); - test('should find groups by name pattern', async () => { - /** Create additional groups */ - await Group.create({ name: 'Test Group 2', source: 'local' }); - await Group.create({ name: 'Admin Group', source: 'local' }); - await Group.create({ - name: 'Test Entra Group', - source: 'entra', - idOnTheSource: 'entra-id-xyz', - }); - - /** Search for all "Test" groups */ - const testGroups = await methods.findGroupsByNamePattern('Test'); - expect(testGroups).toHaveLength(3); - - /** Search with source filter */ - const localTestGroups = await methods.findGroupsByNamePattern('Test', 'local'); - expect(localTestGroups).toHaveLength(2); - - const entraTestGroups = await methods.findGroupsByNamePattern('Test', 'entra'); - expect(entraTestGroups).toHaveLength(1); + it('finds groups by name pattern (case-insensitive)', async () => { + const results = await methods.findGroupsByNamePattern('eng'); + expect(results.length).toBeGreaterThanOrEqual(2); }); - test('should respect limit parameter in name search', async () => { - /** Create many groups with similar names */ - for (let i = 0; i < 10; i++) { + it('matches on email field', async () => { + const results = await methods.findGroupsByNamePattern('design@'); + expect(results).toHaveLength(1); + expect(results[0].name).toBe('Design'); + }); + + it('matches on description field', async () => { + const results = await methods.findGroupsByNamePattern('Eng team'); + expect(results).toHaveLength(1); + expect(results[0].name).toBe('Engineering'); + }); + + it('filters by source when provided', async () => { + const results = await methods.findGroupsByNamePattern('eng', 'entra'); + expect(results).toHaveLength(1); + expect(results[0].source).toBe('entra'); + }); + + it('respects limit parameter', async () => { + for (let i = 0; i < 5; i++) { await Group.create({ name: `Numbered Group ${i}`, source: 'local' }); } - - const limitedGroups = await methods.findGroupsByNamePattern('Numbered', null, 5); - expect(limitedGroups).toHaveLength(5); - }); - - test('should find groups by member ID', async () => { - /** Create additional groups with the test user as member */ - const group2 = await Group.create({ - name: 'Second Group', - source: 'local', - memberIds: [(testUser._id as mongoose.Types.ObjectId).toString()], - }); - - const group3 = await Group.create({ - name: 'Third Group', - source: 'local', - memberIds: [new mongoose.Types.ObjectId().toString()] /** Different user */, - }); - - const userGroups = await methods.findGroupsByMemberId( - testUser._id as mongoose.Types.ObjectId, - ); - expect(userGroups).toHaveLength(2); - - /** IDs should match the groups where user is a member */ - const groupIds = userGroups.map((g) => g._id.toString()); - expect(groupIds).toContain(testGroup._id.toString()); - expect(groupIds).toContain(group2._id.toString()); - expect(groupIds).not.toContain(group3._id.toString()); + const results = await methods.findGroupsByNamePattern('Numbered', null, 2); + expect(results).toHaveLength(2); }); }); - describe('Group Creation and Update Methods', () => { - test('should create a new group', async () => { - const groupData = { - name: 'New Test Group', - source: 'local' as const, - }; + describe('findGroupsByMemberId', () => { + it('returns groups the user is a member of via idOnTheSource', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + await Group.create([ + { name: 'Group A', source: 'local', memberIds: ['user-ext-1'] }, + { name: 'Group B', source: 'local', memberIds: ['user-ext-1'] }, + { name: 'Group C', source: 'local', memberIds: ['other-user'] }, + ]); - const group = await methods.createGroup(groupData); - - expect(group).toBeDefined(); - expect(group.name).toBe(groupData.name); - expect(group.source).toBe(groupData.source); - - /** Verify it was saved to the database */ - const savedGroup = await Group.findById(group._id); - expect(savedGroup).toBeDefined(); + const groups = await methods.findGroupsByMemberId(user._id); + expect(groups).toHaveLength(2); + const names = groups.map((g) => g.name).sort(); + expect(names).toEqual(['Group A', 'Group B']); }); - test('should upsert a group by external ID (create new)', async () => { - const groupData = { - name: 'New Entra Group', - idOnTheSource: 'new-entra-id', - }; - - const group = await methods.upsertGroupByExternalId(groupData.idOnTheSource, 'entra', { - name: groupData.name, - }); - - expect(group).toBeDefined(); - expect(group?.name).toBe(groupData.name); - expect(group?.idOnTheSource).toBe(groupData.idOnTheSource); - expect(group?.source).toBe('entra'); - - /** Verify it was saved to the database */ - const savedGroup = await Group.findOne({ idOnTheSource: 'new-entra-id' }); - expect(savedGroup).toBeDefined(); + it('returns empty array when user does not exist', async () => { + const groups = await methods.findGroupsByMemberId(new Types.ObjectId()); + expect(groups).toEqual([]); }); - test('should upsert a group by external ID (update existing)', async () => { - /** Create an existing group */ + it('falls back to userId string when user has no idOnTheSource', async () => { + const user = await createTestUser(); await Group.create({ - name: 'Original Name', - source: 'entra', - idOnTheSource: 'existing-entra-id', + name: 'Group X', + source: 'local', + memberIds: [user._id.toString()], }); - /** Update it */ - const updatedGroup = await methods.upsertGroupByExternalId('existing-entra-id', 'entra', { + const groups = await methods.findGroupsByMemberId(user._id); + expect(groups).toHaveLength(1); + }); + }); + + describe('createGroup', () => { + it('creates a group and returns the document', async () => { + const group = await methods.createGroup({ name: 'New Group', source: 'local' }); + expect(group).toBeTruthy(); + expect(group.name).toBe('New Group'); + expect(group._id).toBeDefined(); + }); + }); + + describe('upsertGroupByExternalId', () => { + it('creates a new group when none exists', async () => { + const group = await methods.upsertGroupByExternalId('ext-new', 'entra', { + name: 'New Entra Group', + }); + expect(group).toBeTruthy(); + expect(group!.name).toBe('New Entra Group'); + expect(group!.idOnTheSource).toBe('ext-new'); + }); + + it('updates existing group when found', async () => { + await Group.create({ name: 'Old Name', source: 'entra', idOnTheSource: 'ext-1' }); + const group = await methods.upsertGroupByExternalId('ext-1', 'entra', { name: 'Updated Name', }); - - expect(updatedGroup).toBeDefined(); - expect(updatedGroup?.name).toBe('Updated Name'); - expect(updatedGroup?.idOnTheSource).toBe('existing-entra-id'); - - /** Verify the update in the database */ - const savedGroup = await Group.findOne({ idOnTheSource: 'existing-entra-id' }); - expect(savedGroup?.name).toBe('Updated Name'); + expect(group!.name).toBe('Updated Name'); + const count = await Group.countDocuments({ idOnTheSource: 'ext-1' }); + expect(count).toBe(1); }); }); - describe('User-Group Relationship Methods', () => { - let testUser1: t.IUser; - let testGroup: t.IGroup; + describe('addUserToGroup', () => { + it('adds user to group using idOnTheSource', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + const group = await Group.create({ name: 'Team', source: 'local' }); - beforeEach(async () => { - /** Create test users */ - testUser1 = await User.create({ - name: 'User One', - email: 'user1@example.com', - password: 'password123', - provider: 'local', - }); + const { group: updatedGroup } = await methods.addUserToGroup(user._id, group._id); + expect(updatedGroup!.memberIds).toContain('user-ext-1'); + }); - /** Create a test group */ - testGroup = await Group.create({ - name: 'Test Group', + it('falls back to userId string when user has no idOnTheSource', async () => { + const user = await createTestUser(); + const group = await Group.create({ name: 'Team', source: 'local' }); + + const { group: updatedGroup } = await methods.addUserToGroup(user._id, group._id); + expect(updatedGroup!.memberIds).toContain(user._id.toString()); + }); + + it('is idempotent — $addToSet prevents duplicates', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + const group = await Group.create({ name: 'Team', source: 'local' }); + + await methods.addUserToGroup(user._id, group._id); + const { group: updatedGroup } = await methods.addUserToGroup(user._id, group._id); + expect(updatedGroup!.memberIds!.filter((id) => id === 'user-ext-1')).toHaveLength(1); + }); + + it('throws when user does not exist', async () => { + const group = await Group.create({ name: 'Team', source: 'local' }); + await expect(methods.addUserToGroup(new Types.ObjectId(), group._id)).rejects.toThrow( + /User not found/, + ); + }); + }); + + describe('removeUserFromGroup', () => { + it('removes user from group', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + const group = await Group.create({ + name: 'Team', source: 'local', - memberIds: [] /** Initialize empty array */, - }); - }); - - test('should add user to group', async () => { - const result = await methods.addUserToGroup( - testUser1._id as mongoose.Types.ObjectId, - testGroup._id as mongoose.Types.ObjectId, - ); - - /** Verify the result */ - expect(result).toBeDefined(); - expect(result.user).toBeDefined(); - expect(result.group).toBeDefined(); - - /** Group should have the user in memberIds (using idOnTheSource or user ID) */ - const userIdOnTheSource = - result.user.idOnTheSource || (testUser1._id as mongoose.Types.ObjectId).toString(); - expect(result.group?.memberIds).toContain(userIdOnTheSource); - - /** Verify in database */ - const updatedGroup = await Group.findById(testGroup._id); - expect(updatedGroup?.memberIds).toContain(userIdOnTheSource); - }); - - test('should remove user from group', async () => { - /** First add the user to the group */ - await methods.addUserToGroup( - testUser1._id as mongoose.Types.ObjectId, - testGroup._id as mongoose.Types.ObjectId, - ); - - /** Then remove them */ - const result = await methods.removeUserFromGroup( - testUser1._id as mongoose.Types.ObjectId, - testGroup._id as mongoose.Types.ObjectId, - ); - - /** Verify the result */ - expect(result).toBeDefined(); - expect(result.user).toBeDefined(); - expect(result.group).toBeDefined(); - - /** Group should not have the user in memberIds */ - const userIdOnTheSource = - result.user.idOnTheSource || (testUser1._id as mongoose.Types.ObjectId).toString(); - expect(result.group?.memberIds).not.toContain(userIdOnTheSource); - - /** Verify in database */ - const updatedGroup = await Group.findById(testGroup._id); - expect(updatedGroup?.memberIds).not.toContain(userIdOnTheSource); - }); - - test('should get all groups for a user', async () => { - /** Add user to multiple groups */ - const group1 = await Group.create({ name: 'Group 1', source: 'local', memberIds: [] }); - const group2 = await Group.create({ name: 'Group 2', source: 'local', memberIds: [] }); - - await methods.addUserToGroup( - testUser1._id as mongoose.Types.ObjectId, - group1._id as mongoose.Types.ObjectId, - ); - await methods.addUserToGroup( - testUser1._id as mongoose.Types.ObjectId, - group2._id as mongoose.Types.ObjectId, - ); - - /** Get the user's groups */ - const userGroups = await methods.getUserGroups(testUser1._id as mongoose.Types.ObjectId); - - expect(userGroups).toHaveLength(2); - const groupIds = userGroups.map((g) => g._id.toString()); - expect(groupIds).toContain(group1._id.toString()); - expect(groupIds).toContain(group2._id.toString()); - }); - - test('should return empty array for getUserGroups when user has no groups', async () => { - const userGroups = await methods.getUserGroups(testUser1._id as mongoose.Types.ObjectId); - expect(userGroups).toEqual([]); - }); - - test('should get user principals', async () => { - /** Add user to a group */ - await methods.addUserToGroup( - testUser1._id as mongoose.Types.ObjectId, - testGroup._id as mongoose.Types.ObjectId, - ); - - /** Get user principals */ - const principals = await methods.getUserPrincipals({ - userId: testUser1._id as mongoose.Types.ObjectId, + memberIds: ['user-ext-1'], }); - /** Should include user, role (default USER), group, and public principals */ - expect(principals).toHaveLength(4); - - /** Check principal types */ - const userPrincipal = principals.find((p) => p.principalType === PrincipalType.USER); - const groupPrincipal = principals.find((p) => p.principalType === PrincipalType.GROUP); - const publicPrincipal = principals.find((p) => p.principalType === PrincipalType.PUBLIC); - - expect(userPrincipal).toBeDefined(); - expect(userPrincipal?.principalId?.toString()).toBe( - (testUser1._id as mongoose.Types.ObjectId).toString(), - ); - - expect(groupPrincipal).toBeDefined(); - expect(groupPrincipal?.principalId?.toString()).toBe(testGroup._id.toString()); - - expect(publicPrincipal).toBeDefined(); - expect(publicPrincipal?.principalId).toBeUndefined(); + const { group: updatedGroup } = await methods.removeUserFromGroup(user._id, group._id); + expect(updatedGroup!.memberIds).not.toContain('user-ext-1'); }); - test('should return user and public principals for non-existent user in getUserPrincipals', async () => { - const nonExistentId = new mongoose.Types.ObjectId(); - const principals = await methods.getUserPrincipals({ - userId: nonExistentId, - }); - - /** Should still return user and public principals even for non-existent user */ - expect(principals).toHaveLength(2); - expect(principals[0].principalType).toBe(PrincipalType.USER); - expect(principals[0].principalId?.toString()).toBe(nonExistentId.toString()); - expect(principals[1].principalType).toBe(PrincipalType.PUBLIC); - expect(principals[1].principalId).toBeUndefined(); + it('throws when user does not exist', async () => { + const group = await Group.create({ name: 'Team', source: 'local' }); + await expect(methods.removeUserFromGroup(new Types.ObjectId(), group._id)).rejects.toThrow( + /User not found/, + ); }); - test('should convert string userId to ObjectId in getUserPrincipals', async () => { - /** Add user to a group */ - await methods.addUserToGroup( - testUser1._id as mongoose.Types.ObjectId, - testGroup._id as mongoose.Types.ObjectId, - ); - - /** Get user principals with string userId */ - const principals = await methods.getUserPrincipals({ - userId: (testUser1._id as mongoose.Types.ObjectId).toString(), + it('is safe when user is not a member', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + const group = await Group.create({ + name: 'Team', + source: 'local', + memberIds: ['other-user'], }); - /** Should include user, role (default USER), group, and public principals */ - expect(principals).toHaveLength(4); - - /** Check that USER principal has ObjectId */ - const userPrincipal = principals.find((p) => p.principalType === PrincipalType.USER); - expect(userPrincipal).toBeDefined(); - expect(userPrincipal?.principalId).toBeInstanceOf(mongoose.Types.ObjectId); - expect(userPrincipal?.principalId?.toString()).toBe( - (testUser1._id as mongoose.Types.ObjectId).toString(), - ); - - /** Check that GROUP principal has ObjectId */ - const groupPrincipal = principals.find((p) => p.principalType === PrincipalType.GROUP); - expect(groupPrincipal).toBeDefined(); - expect(groupPrincipal?.principalId).toBeInstanceOf(mongoose.Types.ObjectId); - expect(groupPrincipal?.principalId?.toString()).toBe(testGroup._id.toString()); - }); - - test('should include role principal as string in getUserPrincipals', async () => { - /** Create user with specific role */ - const userWithRole = await User.create({ - name: 'Admin User', - email: 'admin@example.com', - password: 'password123', - provider: 'local', - role: 'ADMIN', - }); - - /** Get user principals */ - const principals = await methods.getUserPrincipals({ - userId: userWithRole._id as mongoose.Types.ObjectId, - }); - - /** Should include user, role, and public principals */ - expect(principals).toHaveLength(3); - - /** Check that ROLE principal has string ID */ - const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); - expect(rolePrincipal).toBeDefined(); - expect(typeof rolePrincipal?.principalId).toBe('string'); - expect(rolePrincipal?.principalId).toBe('ADMIN'); + const { group: updatedGroup } = await methods.removeUserFromGroup(user._id, group._id); + expect(updatedGroup!.memberIds).toEqual(['other-user']); }); }); - describe('Entra ID Synchronization', () => { - let testUser: t.IUser; + describe('removeUserFromAllGroups', () => { + it('removes user from every group they belong to', async () => { + const userId = new Types.ObjectId(); + await Group.create([ + { name: 'Group A', source: 'local', memberIds: [userId.toString(), 'other'] }, + { name: 'Group B', source: 'local', memberIds: [userId.toString()] }, + { name: 'Group C', source: 'local', memberIds: ['other'] }, + ]); - beforeEach(async () => { - testUser = await User.create({ - name: 'Entra User', - email: 'entra@example.com', - password: 'password123', - provider: 'entra', - idOnTheSource: 'entra-user-123', - }); + await methods.removeUserFromAllGroups(userId.toString()); + + const groups = await Group.find({ memberIds: userId.toString() }); + expect(groups).toHaveLength(0); + + const groupC = await Group.findOne({ name: 'Group C' }); + expect(groupC!.memberIds).toContain('other'); }); - /** Skip the failing tests until they can be fixed properly */ - test.skip('should sync Entra groups for a user (add new groups)', async () => { - /** Mock Entra groups */ - const entraGroups = [ - { id: 'entra-group-1', name: 'Entra Group 1' }, - { id: 'entra-group-2', name: 'Entra Group 2' }, - ]; + it('is a no-op when user is not in any groups', async () => { + await Group.create({ name: 'Group A', source: 'local', memberIds: ['other'] }); + await expect( + methods.removeUserFromAllGroups(new Types.ObjectId().toString()), + ).resolves.not.toThrow(); + }); + }); - const result = await methods.syncUserEntraGroups( - testUser._id as mongoose.Types.ObjectId, - entraGroups, - ); + describe('getUserGroups', () => { + it('delegates to findGroupsByMemberId', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + await Group.create({ name: 'Team', source: 'local', memberIds: ['user-ext-1'] }); - /** Check result */ - expect(result).toBeDefined(); - expect(result.user).toBeDefined(); - expect(result.addedGroups).toHaveLength(2); - expect(result.removedGroups).toHaveLength(0); + const groups = await methods.getUserGroups(user._id); + expect(groups).toHaveLength(1); + expect(groups[0].name).toBe('Team'); + }); + }); + + describe('getUserPrincipals', () => { + it('returns USER, ROLE, and PUBLIC principals', async () => { + const user = await createTestUser({ role: SystemRoles.ADMIN }); + const principals = await methods.getUserPrincipals({ + userId: user._id.toString(), + role: SystemRoles.ADMIN, + }); + + const types = principals.map((p) => p.principalType); + expect(types).toContain(PrincipalType.USER); + expect(types).toContain(PrincipalType.ROLE); + expect(types).toContain(PrincipalType.PUBLIC); + }); + + it('includes group principals when user is a member', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + const group = await Group.create({ + name: 'Team', + source: 'local', + memberIds: ['user-ext-1'], + }); + + const principals = await methods.getUserPrincipals({ + userId: user._id.toString(), + role: SystemRoles.USER, + }); + + const groupPrincipal = principals.find((p) => p.principalType === PrincipalType.GROUP); + expect(groupPrincipal).toBeTruthy(); + expect(groupPrincipal!.principalId!.toString()).toBe(group._id.toString()); + }); + + it('queries user role from DB when role param is undefined', async () => { + const user = await createTestUser({ role: SystemRoles.ADMIN }); + const principals = await methods.getUserPrincipals({ userId: user._id.toString() }); + + const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); + expect(rolePrincipal).toBeTruthy(); + expect(rolePrincipal!.principalId).toBe(SystemRoles.ADMIN); + }); + + it('omits role principal when role is empty/whitespace', async () => { + const user = await createTestUser({ role: ' ' }); + const principals = await methods.getUserPrincipals({ + userId: user._id.toString(), + role: ' ', + }); + + const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); + expect(rolePrincipal).toBeUndefined(); + }); + + it('converts string userId to ObjectId for USER principal', async () => { + const user = await createTestUser(); + const principals = await methods.getUserPrincipals({ + userId: user._id.toString(), + role: SystemRoles.USER, + }); + + const userPrincipal = principals.find((p) => p.principalType === PrincipalType.USER); + expect(userPrincipal!.principalId).toBeInstanceOf(Types.ObjectId); + }); + + it('includes null role when role param is null', async () => { + const user = await createTestUser({ role: SystemRoles.USER }); + const principals = await methods.getUserPrincipals({ + userId: user._id.toString(), + role: null, + }); + + const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); + expect(rolePrincipal).toBeUndefined(); + }); + }); + + describe('syncUserEntraGroups', () => { + it('creates new groups and adds user as member', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + + const { addedGroups, removedGroups } = await methods.syncUserEntraGroups(user._id, [ + { id: 'entra-g1', name: 'Entra Group 1' }, + { id: 'entra-g2', name: 'Entra Group 2', description: 'desc', email: 'g2@co.com' }, + ]); + + expect(addedGroups).toHaveLength(2); + expect(removedGroups).toHaveLength(0); - /** Verify groups were created */ const groups = await Group.find({ source: 'entra' }); expect(groups).toHaveLength(2); - - /** Verify user is a member of both groups - skipping this assertion for now */ - const user = await User.findById(testUser._id); - expect(user).toBeDefined(); - - /** Verify each group has the user as a member */ - for (const group of groups) { - expect(group.memberIds).toContain( - testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), - ); - } + expect(groups.every((g) => g.memberIds!.includes('user-ext-1'))).toBe(true); }); - test.skip('should sync Entra groups for a user (add and remove groups)', async () => { - /** Create existing Entra groups for the user */ + it('adds user to existing group they are not a member of', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); await Group.create({ - name: 'Existing Group 1', + name: 'Existing Entra Group', source: 'entra', - idOnTheSource: 'existing-1', - memberIds: [testUser.idOnTheSource], + idOnTheSource: 'entra-g1', + memberIds: ['other-user'], }); - const existingGroup2 = await Group.create({ - name: 'Existing Group 2', + const { addedGroups } = await methods.syncUserEntraGroups(user._id, [ + { id: 'entra-g1', name: 'Existing Entra Group' }, + ]); + + expect(addedGroups).toHaveLength(1); + const group = await Group.findOne({ idOnTheSource: 'entra-g1' }); + expect(group!.memberIds).toContain('user-ext-1'); + expect(group!.memberIds).toContain('other-user'); + }); + + it('skips groups the user is already a member of', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + await Group.create({ + name: 'Already Member', source: 'entra', - idOnTheSource: 'existing-2', - memberIds: [testUser.idOnTheSource], + idOnTheSource: 'entra-g1', + memberIds: ['user-ext-1'], }); - /** Groups already have user in memberIds from creation above */ + const { addedGroups } = await methods.syncUserEntraGroups(user._id, [ + { id: 'entra-g1', name: 'Already Member' }, + ]); - /** New Entra groups (one existing, one new) */ - const entraGroups = [ - { id: 'existing-1', name: 'Existing Group 1' } /** Keep this one */, - { id: 'new-group', name: 'New Group' } /** Add this one */, - /** existing-2 is missing, should be removed */ - ]; - - const result = await methods.syncUserEntraGroups( - testUser._id as mongoose.Types.ObjectId, - entraGroups, - ); - - /** Check result */ - expect(result).toBeDefined(); - expect(result.addedGroups).toHaveLength(1); /** Skipping exact array length expectations */ - expect(result.removedGroups).toHaveLength(1); - - /** Verify existing-2 no longer has user as member */ - const removedGroup = await Group.findById(existingGroup2._id); - expect(removedGroup?.memberIds).toHaveLength(0); - - /** Verify new group was created and has user as member */ - const newGroup = await Group.findOne({ idOnTheSource: 'new-group' }); - expect(newGroup).toBeDefined(); - expect(newGroup?.memberIds).toContain( - testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), - ); + expect(addedGroups).toHaveLength(0); }); - test('should throw error for non-existent user in syncUserEntraGroups', async () => { - const nonExistentId = new mongoose.Types.ObjectId(); - const entraGroups = [{ id: 'some-id', name: 'Some Group' }]; + it('removes user from stale entra groups', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + await Group.create({ + name: 'Stale Group', + source: 'entra', + idOnTheSource: 'entra-stale', + memberIds: ['user-ext-1'], + }); - await expect(methods.syncUserEntraGroups(nonExistentId, entraGroups)).rejects.toThrow( - 'User not found', - ); + const { removedGroups } = await methods.syncUserEntraGroups(user._id, []); + + expect(removedGroups).toHaveLength(1); + expect(removedGroups[0].name).toBe('Stale Group'); + const group = await Group.findOne({ idOnTheSource: 'entra-stale' }); + expect(group!.memberIds).not.toContain('user-ext-1'); }); - test.skip('should preserve local groups when syncing Entra groups', async () => { - /** Create a local group for the user */ - const localGroup = await Group.create({ + it('handles add-and-remove in one sync call', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + + await Group.create({ + name: 'Keep Group', + source: 'entra', + idOnTheSource: 'entra-keep', + memberIds: ['user-ext-1'], + }); + await Group.create({ + name: 'Remove Group', + source: 'entra', + idOnTheSource: 'entra-remove', + memberIds: ['user-ext-1'], + }); + + const { addedGroups, removedGroups } = await methods.syncUserEntraGroups(user._id, [ + { id: 'entra-keep', name: 'Keep Group' }, + { id: 'entra-new', name: 'New Group' }, + ]); + + expect(addedGroups).toHaveLength(1); + expect(addedGroups[0].name).toBe('New Group'); + expect(removedGroups).toHaveLength(1); + expect(removedGroups[0].name).toBe('Remove Group'); + }); + + it('preserves local groups during entra sync', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + await Group.create({ name: 'Local Group', source: 'local', - memberIds: [testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString()], + memberIds: ['user-ext-1'], }); - /** Group already has user in memberIds from creation above */ + await methods.syncUserEntraGroups(user._id, []); - /** Sync with Entra groups */ - const entraGroups = [{ id: 'entra-group', name: 'Entra Group' }]; + const localGroup = await Group.findOne({ name: 'Local Group' }); + expect(localGroup!.memberIds).toContain('user-ext-1'); + }); - const result = await methods.syncUserEntraGroups( - testUser._id as mongoose.Types.ObjectId, - entraGroups, + it('throws when user does not exist', async () => { + await expect( + methods.syncUserEntraGroups(new Types.ObjectId(), [{ id: 'g1', name: 'Group' }]), + ).rejects.toThrow(/User not found/); + }); + + it('returns the updated user document', async () => { + const user = await createTestUser({ idOnTheSource: 'user-ext-1' }); + const { user: updatedUser } = await methods.syncUserEntraGroups(user._id, []); + expect(updatedUser._id.toString()).toBe(user._id.toString()); + }); + }); + + describe('calculateRelevanceScore', () => { + it('returns 100 for exact match', () => { + const score = methods.calculateRelevanceScore( + { type: PrincipalType.USER, name: 'alice', source: 'local' }, + 'alice', + ); + expect(score).toBe(100); + }); + + it('returns 80 for starts-with match', () => { + const score = methods.calculateRelevanceScore( + { type: PrincipalType.USER, name: 'alice-smith', source: 'local' }, + 'alice', + ); + expect(score).toBe(80); + }); + + it('returns 50 for contains match', () => { + const score = methods.calculateRelevanceScore( + { type: PrincipalType.USER, name: 'bob-alice-jones', source: 'local' }, + 'alice', + ); + expect(score).toBe(50); + }); + + it('returns 10 (default) when no substring or exact match — regex fallback', () => { + const score = methods.calculateRelevanceScore( + { type: PrincipalType.USER, name: 'bob', source: 'local' }, + 'zzz', + ); + expect(score).toBe(10); + }); + + it('checks email and username for USER type', () => { + const score = methods.calculateRelevanceScore( + { + type: PrincipalType.USER, + name: 'other', + email: 'alice@test.com', + username: 'alice', + source: 'local', + }, + 'alice', + ); + expect(score).toBe(100); + }); + + it('checks description for GROUP type', () => { + const score = methods.calculateRelevanceScore( + { + type: PrincipalType.GROUP, + name: 'other', + description: 'alice team', + source: 'local', + }, + 'alice', + ); + expect(score).toBe(80); + }); + + it('picks the highest score across multiple fields', () => { + const score = methods.calculateRelevanceScore( + { + type: PrincipalType.USER, + name: 'contains-alice-here', + email: 'alice@test.com', + source: 'local', + }, + 'alice', + ); + expect(score).toBe(80); + }); + + it('returns 100 when regex pattern matches exactly via dot wildcard', () => { + const score = methods.calculateRelevanceScore( + { type: PrincipalType.USER, name: 'xYz', source: 'local' }, + 'x.z', + ); + expect(score).toBe(100); + }); + }); + + describe('sortPrincipalsByRelevance', () => { + it('sorts by score descending', () => { + const items = [ + { type: PrincipalType.USER, name: 'low', _searchScore: 10 }, + { type: PrincipalType.USER, name: 'high', _searchScore: 100 }, + { type: PrincipalType.USER, name: 'mid', _searchScore: 50 }, + ]; + + const sorted = methods.sortPrincipalsByRelevance(items); + expect(sorted.map((i) => i._searchScore)).toEqual([100, 50, 10]); + }); + + it('prioritizes USER over GROUP at equal scores', () => { + const items = [ + { type: PrincipalType.GROUP, name: 'group', _searchScore: 80 }, + { type: PrincipalType.USER, name: 'user', _searchScore: 80 }, + ]; + + const sorted = methods.sortPrincipalsByRelevance(items); + expect(sorted[0].type).toBe(PrincipalType.USER); + }); + + it('sorts alphabetically by name at equal scores and types', () => { + const items = [ + { type: PrincipalType.USER, name: 'charlie', _searchScore: 80 }, + { type: PrincipalType.USER, name: 'alice', _searchScore: 80 }, + { type: PrincipalType.USER, name: 'bob', _searchScore: 80 }, + ]; + + const sorted = methods.sortPrincipalsByRelevance(items); + expect(sorted.map((i) => i.name)).toEqual(['alice', 'bob', 'charlie']); + }); + + it('handles missing _searchScore (falls back to 0)', () => { + const items = [ + { type: PrincipalType.USER, name: 'a' }, + { type: PrincipalType.USER, name: 'b', _searchScore: 50 }, + ]; + + const sorted = methods.sortPrincipalsByRelevance(items); + expect(sorted[0]._searchScore).toBe(50); + }); + + it('uses email as fallback name for sorting', () => { + const items = [ + { type: PrincipalType.USER, email: 'z@test.com', _searchScore: 80 }, + { type: PrincipalType.USER, email: 'a@test.com', _searchScore: 80 }, + ]; + + const sorted = methods.sortPrincipalsByRelevance(items); + expect(sorted[0].email).toBe('a@test.com'); + }); + }); + + describe('searchPrincipals', () => { + beforeEach(async () => { + await User.create([ + { + name: 'Alice Smith', + email: 'alice@test.com', + username: 'alice', + password: 'password123', + provider: 'local', + }, + { + name: 'Bob Jones', + email: 'bob@test.com', + username: 'bob', + password: 'password123', + provider: 'local', + }, + ]); + await Group.create([ + { name: 'Alpha Team', source: 'local' }, + { name: 'Beta Team', source: 'local' }, + ]); + await Role.create([{ name: 'admin' }, { name: 'moderator' }]); + }); + + it('returns empty array for empty search pattern', async () => { + const results = await methods.searchPrincipals(''); + expect(results).toEqual([]); + }); + + it('returns empty array for whitespace-only pattern', async () => { + const results = await methods.searchPrincipals(' '); + expect(results).toEqual([]); + }); + + it('finds matching users', async () => { + const results = await methods.searchPrincipals('alice'); + const userResults = results.filter((r) => r.type === PrincipalType.USER); + expect(userResults.length).toBeGreaterThanOrEqual(1); + expect(userResults[0].name).toBe('Alice Smith'); + }); + + it('finds matching groups', async () => { + const results = await methods.searchPrincipals('alpha'); + const groupResults = results.filter((r) => r.type === PrincipalType.GROUP); + expect(groupResults.length).toBeGreaterThanOrEqual(1); + expect(groupResults[0].name).toBe('Alpha Team'); + }); + + it('finds matching roles', async () => { + const results = await methods.searchPrincipals('admin'); + const roleResults = results.filter((r) => r.type === PrincipalType.ROLE); + expect(roleResults.length).toBeGreaterThanOrEqual(1); + expect(roleResults[0].name).toBe('admin'); + }); + + it('filters by USER type only', async () => { + const results = await methods.searchPrincipals('a', 10, [PrincipalType.USER]); + expect(results.every((r) => r.type === PrincipalType.USER)).toBe(true); + }); + + it('filters by GROUP type only', async () => { + const results = await methods.searchPrincipals('team', 10, [PrincipalType.GROUP]); + expect(results.every((r) => r.type === PrincipalType.GROUP)).toBe(true); + expect(results.length).toBeGreaterThanOrEqual(1); + }); + + it('filters by ROLE type only', async () => { + const results = await methods.searchPrincipals('mod', 10, [PrincipalType.ROLE]); + expect(results.every((r) => r.type === PrincipalType.ROLE)).toBe(true); + expect(results.length).toBeGreaterThanOrEqual(1); + }); + + it('respects limitPerType', async () => { + const results = await methods.searchPrincipals('a', 1); + const userResults = results.filter((r) => r.type === PrincipalType.USER); + expect(userResults.length).toBeLessThanOrEqual(1); + }); + + it('returns combined results across types without filter', async () => { + const results = await methods.searchPrincipals('a'); + const types = new Set(results.map((r) => r.type)); + expect(types.size).toBeGreaterThanOrEqual(2); + }); + + it('finds users by username', async () => { + const results = await methods.searchPrincipals('alice', 10, [PrincipalType.USER]); + expect(results.length).toBeGreaterThanOrEqual(1); + }); + + it('transforms user results to TPrincipalSearchResult format', async () => { + const results = await methods.searchPrincipals('alice', 10, [PrincipalType.USER]); + expect(results[0]).toEqual( + expect.objectContaining({ + type: PrincipalType.USER, + name: 'Alice Smith', + source: 'local', + }), + ); + expect(results[0].id).toBeDefined(); + }); + + it('transforms group results to TPrincipalSearchResult format', async () => { + const results = await methods.searchPrincipals('alpha', 10, [PrincipalType.GROUP]); + expect(results[0]).toEqual( + expect.objectContaining({ + type: PrincipalType.GROUP, + name: 'Alpha Team', + source: 'local', + }), + ); + expect(results[0].id).toBeDefined(); + expect(results[0].memberCount).toBeDefined(); + }); + }); + + describe('findGroupByQuery', () => { + it('finds a group by custom filter', async () => { + await Group.create({ name: 'Target', source: 'local', email: 'target@co.com' }); + const found = await methods.findGroupByQuery({ email: 'target@co.com' }); + expect(found).toBeTruthy(); + expect(found!.name).toBe('Target'); + }); + + it('returns null when no match', async () => { + const found = await methods.findGroupByQuery({ name: 'Nonexistent' }); + expect(found).toBeNull(); + }); + }); + + describe('updateGroupById', () => { + it('updates the group and returns the new document', async () => { + const group = await Group.create({ name: 'Old Name', source: 'local' }); + const updated = await methods.updateGroupById(group._id, { name: 'New Name' }); + expect(updated!.name).toBe('New Name'); + }); + + it('returns null when group does not exist', async () => { + const updated = await methods.updateGroupById(new Types.ObjectId(), { name: 'X' }); + expect(updated).toBeNull(); + }); + }); + + describe('bulkUpdateGroups', () => { + it('updates all groups matching the filter', async () => { + await Group.create([ + { name: 'Group A', source: 'entra', idOnTheSource: 'ext-a' }, + { name: 'Group B', source: 'entra', idOnTheSource: 'ext-b' }, + { name: 'Group C', source: 'local' }, + ]); + + const result = await methods.bulkUpdateGroups( + { source: 'entra' }, + { $set: { description: 'synced' } }, ); - /** Check result */ - expect(result).toBeDefined(); + expect(result.modifiedCount).toBe(2); + const synced = await Group.find({ description: 'synced' }); + expect(synced).toHaveLength(2); + }); - /** Verify the local group entry still exists */ - const savedLocalGroup = await Group.findById(localGroup._id); - expect(savedLocalGroup).toBeDefined(); - expect(savedLocalGroup?.memberIds).toContain( - testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), - ); - - /** Verify the Entra group was created */ - const entraGroup = await Group.findOne({ idOnTheSource: 'entra-group' }); - expect(entraGroup).toBeDefined(); - expect(entraGroup?.memberIds).toContain( - testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), + it('returns zero when no groups match', async () => { + const result = await methods.bulkUpdateGroups( + { source: 'entra' }, + { $set: { description: 'x' } }, ); + expect(result.modifiedCount).toBe(0); }); }); }); diff --git a/packages/data-schemas/src/methods/userGroup.ts b/packages/data-schemas/src/methods/userGroup.ts index 5c683268b3..5e11c26135 100644 --- a/packages/data-schemas/src/methods/userGroup.ts +++ b/packages/data-schemas/src/methods/userGroup.ts @@ -244,6 +244,13 @@ export function createUserGroupMethods(mongoose: typeof import('mongoose')) { * @param session - Optional MongoDB session for transactions * @returns Array of principal objects with type and id */ + /** + * TODO(#12091): This method has no tenantId parameter — it returns ALL group + * memberships for a user regardless of tenant. In multi-tenant mode, group + * principals from other tenants will be included in capability checks, which + * could grant cross-tenant capabilities. Add tenantId filtering here when + * tenant isolation is activated. + */ async function getUserPrincipals( params: { userId: string | Types.ObjectId; diff --git a/packages/data-schemas/src/models/index.ts b/packages/data-schemas/src/models/index.ts index 068aba69ed..44d94c6ab4 100644 --- a/packages/data-schemas/src/models/index.ts +++ b/packages/data-schemas/src/models/index.ts @@ -25,6 +25,7 @@ import { createToolCallModel } from './toolCall'; import { createMemoryModel } from './memory'; import { createAccessRoleModel } from './accessRole'; import { createAclEntryModel } from './aclEntry'; +import { createSystemGrantModel } from './systemGrant'; import { createGroupModel } from './group'; /** @@ -59,6 +60,7 @@ export function createModels(mongoose: typeof import('mongoose')) { MemoryEntry: createMemoryModel(mongoose), AccessRole: createAccessRoleModel(mongoose), AclEntry: createAclEntryModel(mongoose), + SystemGrant: createSystemGrantModel(mongoose), Group: createGroupModel(mongoose), }; } diff --git a/packages/data-schemas/src/models/systemGrant.ts b/packages/data-schemas/src/models/systemGrant.ts new file mode 100644 index 0000000000..e439d2af81 --- /dev/null +++ b/packages/data-schemas/src/models/systemGrant.ts @@ -0,0 +1,11 @@ +import systemGrantSchema from '~/schema/systemGrant'; +import type * as t from '~/types'; + +/** + * Creates or returns the SystemGrant model using the provided mongoose instance and schema + */ +export function createSystemGrantModel(mongoose: typeof import('mongoose')) { + return ( + mongoose.models.SystemGrant || mongoose.model('SystemGrant', systemGrantSchema) + ); +} diff --git a/packages/data-schemas/src/schema/index.ts b/packages/data-schemas/src/schema/index.ts index 2a58f7c3cc..456eb03ac2 100644 --- a/packages/data-schemas/src/schema/index.ts +++ b/packages/data-schemas/src/schema/index.ts @@ -24,3 +24,4 @@ export { default as transactionSchema } from './transaction'; export { default as userSchema } from './user'; export { default as memorySchema } from './memory'; export { default as groupSchema } from './group'; +export { default as systemGrantSchema } from './systemGrant'; diff --git a/packages/data-schemas/src/schema/systemGrant.ts b/packages/data-schemas/src/schema/systemGrant.ts new file mode 100644 index 0000000000..0366f6080d --- /dev/null +++ b/packages/data-schemas/src/schema/systemGrant.ts @@ -0,0 +1,76 @@ +import { Schema } from 'mongoose'; +import { PrincipalType } from 'librechat-data-provider'; +import { SystemCapabilities } from '~/systemCapabilities'; +import type { SystemCapability } from '~/systemCapabilities'; +import type { ISystemGrant } from '~/types'; + +const baseCapabilities = new Set(Object.values(SystemCapabilities)); +const sectionCapPattern = /^(?:manage|read):configs:\w+$/; +const assignCapPattern = /^assign:configs:(?:user|group|role)$/; + +const systemGrantSchema = new Schema( + { + principalType: { + type: String, + enum: Object.values(PrincipalType), + required: true, + }, + principalId: { + type: Schema.Types.Mixed, + required: true, + }, + capability: { + type: String, + required: true, + validate: { + validator: (v: SystemCapability) => + baseCapabilities.has(v) || sectionCapPattern.test(v) || assignCapPattern.test(v), + message: 'Invalid capability string: "{VALUE}"', + }, + }, + /** + * Platform-level grants MUST omit this field entirely — never set it to null. + * Queries for platform-level grants use `{ tenantId: { $exists: false } }`, which + * matches absent fields but NOT `null`. A document stored with `{ tenantId: null }` + * would silently match neither platform-level nor tenant-scoped queries. + */ + tenantId: { + type: String, + required: false, + validate: { + validator: (v: unknown) => v !== null && v !== '', + message: 'tenantId must be a non-empty string or omitted entirely — never null or empty', + }, + }, + grantedBy: { + type: Schema.Types.ObjectId, + ref: 'User', + }, + grantedAt: { + type: Date, + default: Date.now, + }, + /** Reserved for future TTL enforcement — time-bounded / temporary grants. Not enforced yet. */ + expiresAt: { + type: Date, + required: false, + }, + }, + { timestamps: true }, +); + +/* + * principalId normalization (string → ObjectId for USER/GROUP) is handled + * explicitly by grantCapability — the only sanctioned write path. + * All writes MUST go through grantCapability; do not use Model.create() + * or save() directly, as there is no schema-level normalization hook. + */ + +systemGrantSchema.index( + { principalType: 1, principalId: 1, capability: 1, tenantId: 1 }, + { unique: true }, +); + +systemGrantSchema.index({ capability: 1, tenantId: 1 }); + +export default systemGrantSchema; diff --git a/packages/data-schemas/src/systemCapabilities.ts b/packages/data-schemas/src/systemCapabilities.ts new file mode 100644 index 0000000000..cf2acfbf88 --- /dev/null +++ b/packages/data-schemas/src/systemCapabilities.ts @@ -0,0 +1,106 @@ +import type { z } from 'zod'; +import type { configSchema } from 'librechat-data-provider'; +import { ResourceType } from 'librechat-data-provider'; + +export const SystemCapabilities = { + ACCESS_ADMIN: 'access:admin', + READ_USERS: 'read:users', + MANAGE_USERS: 'manage:users', + READ_GROUPS: 'read:groups', + MANAGE_GROUPS: 'manage:groups', + READ_ROLES: 'read:roles', + MANAGE_ROLES: 'manage:roles', + READ_CONFIGS: 'read:configs', + MANAGE_CONFIGS: 'manage:configs', + ASSIGN_CONFIGS: 'assign:configs', + READ_USAGE: 'read:usage', + READ_AGENTS: 'read:agents', + MANAGE_AGENTS: 'manage:agents', + MANAGE_MCP_SERVERS: 'manage:mcpservers', + READ_PROMPTS: 'read:prompts', + MANAGE_PROMPTS: 'manage:prompts', + /** Reserved — not yet enforced by any middleware. Grant has no effect until assistant listing is gated. */ + READ_ASSISTANTS: 'read:assistants', + MANAGE_ASSISTANTS: 'manage:assistants', +} as const; + +/** Top-level keys of the configSchema from librechat.yaml. */ +export type ConfigSection = keyof z.infer; + +/** Principal types that can receive config overrides. */ +export type ConfigAssignTarget = 'user' | 'group' | 'role'; + +/** Base capabilities defined in the SystemCapabilities object. */ +type BaseSystemCapability = (typeof SystemCapabilities)[keyof typeof SystemCapabilities]; + +/** Section-level config capabilities derived from configSchema keys. */ +type ConfigSectionCapability = `manage:configs:${ConfigSection}` | `read:configs:${ConfigSection}`; + +/** Principal-scoped config assignment capabilities. */ +type ConfigAssignCapability = `assign:configs:${ConfigAssignTarget}`; + +/** + * Union of all valid capability strings: + * - Base capabilities from SystemCapabilities + * - Section-level config capabilities (manage:configs:
, read:configs:
) + * - Config assignment capabilities (assign:configs:) + */ +export type SystemCapability = + | BaseSystemCapability + | ConfigSectionCapability + | ConfigAssignCapability; + +/** + * Capabilities that are implied by holding a broader capability. + * When `hasCapability` checks for an implied capability, it first expands + * the principal's grant set — so granting `MANAGE_USERS` automatically + * satisfies a `READ_USERS` check without a separate grant. + * + * Implication is one-directional: `MANAGE_USERS` implies `READ_USERS`, + * but `READ_USERS` does NOT imply `MANAGE_USERS`. + */ +export const CapabilityImplications: Partial> = + { + [SystemCapabilities.MANAGE_USERS]: [SystemCapabilities.READ_USERS], + [SystemCapabilities.MANAGE_GROUPS]: [SystemCapabilities.READ_GROUPS], + [SystemCapabilities.MANAGE_ROLES]: [SystemCapabilities.READ_ROLES], + [SystemCapabilities.MANAGE_CONFIGS]: [SystemCapabilities.READ_CONFIGS], + [SystemCapabilities.MANAGE_AGENTS]: [SystemCapabilities.READ_AGENTS], + [SystemCapabilities.MANAGE_PROMPTS]: [SystemCapabilities.READ_PROMPTS], + [SystemCapabilities.MANAGE_ASSISTANTS]: [SystemCapabilities.READ_ASSISTANTS], + }; + +/** + * Maps each ACL ResourceType to the SystemCapability that grants + * unrestricted management access. Typed as `Record` + * so adding a new ResourceType variant causes a compile error until a + * capability is assigned here. + */ +export const ResourceCapabilityMap: Record = { + [ResourceType.AGENT]: SystemCapabilities.MANAGE_AGENTS, + [ResourceType.PROMPTGROUP]: SystemCapabilities.MANAGE_PROMPTS, + [ResourceType.MCPSERVER]: SystemCapabilities.MANAGE_MCP_SERVERS, + [ResourceType.REMOTE_AGENT]: SystemCapabilities.MANAGE_AGENTS, +}; + +/** + * Derives a section-level config management capability from a configSchema key. + * @example configCapability('endpoints') → 'manage:configs:endpoints' + * + * TODO: Section-level config capabilities are scaffolded but not yet active. + * To activate delegated config management: + * 1. Expose POST/DELETE /api/admin/grants endpoints (wiring grantCapability/revokeCapability) + * 2. Seed section-specific grants for delegated admin roles via those endpoints + * 3. Guard config write handlers with hasConfigCapability(user, section) + */ +export function configCapability(section: ConfigSection): `manage:configs:${ConfigSection}` { + return `manage:configs:${section}`; +} + +/** + * Derives a section-level config read capability from a configSchema key. + * @example readConfigCapability('endpoints') → 'read:configs:endpoints' + */ +export function readConfigCapability(section: ConfigSection): `read:configs:${ConfigSection}` { + return `read:configs:${section}`; +} diff --git a/packages/data-schemas/src/types/index.ts b/packages/data-schemas/src/types/index.ts index d467d99d21..26238cbda1 100644 --- a/packages/data-schemas/src/types/index.ts +++ b/packages/data-schemas/src/types/index.ts @@ -26,6 +26,7 @@ export * from './prompts'; /* Access Control */ export * from './accessRole'; export * from './aclEntry'; +export * from './systemGrant'; export * from './group'; /* Web */ export * from './web'; diff --git a/packages/data-schemas/src/types/systemGrant.ts b/packages/data-schemas/src/types/systemGrant.ts new file mode 100644 index 0000000000..9f0d576503 --- /dev/null +++ b/packages/data-schemas/src/types/systemGrant.ts @@ -0,0 +1,25 @@ +import type { Document, Types } from 'mongoose'; +import type { PrincipalType } from 'librechat-data-provider'; +import type { SystemCapability } from '~/systemCapabilities'; + +export type SystemGrant = { + /** The type of principal — matches PrincipalType enum values */ + principalType: PrincipalType; + /** ObjectId string for user/group, role name string for role */ + principalId: string | Types.ObjectId; + /** The capability being granted */ + capability: SystemCapability; + /** Absent = platform-operator, present = tenant-scoped */ + tenantId?: string; + /** ID of the user who granted this capability */ + grantedBy?: Types.ObjectId; + /** When this capability was granted */ + grantedAt?: Date; + /** Reserved for future TTL enforcement — time-bounded / temporary grants. */ + expiresAt?: Date; +}; + +export type ISystemGrant = SystemGrant & + Document & { + _id: Types.ObjectId; + }; diff --git a/packages/data-schemas/src/utils/index.ts b/packages/data-schemas/src/utils/index.ts index 626233f1be..a185a096eb 100644 --- a/packages/data-schemas/src/utils/index.ts +++ b/packages/data-schemas/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './principal'; export * from './string'; export * from './tempChatRetention'; export * from './transactions'; diff --git a/packages/data-schemas/src/utils/principal.ts b/packages/data-schemas/src/utils/principal.ts new file mode 100644 index 0000000000..d8ecb28303 --- /dev/null +++ b/packages/data-schemas/src/utils/principal.ts @@ -0,0 +1,22 @@ +import { Types } from 'mongoose'; +import { PrincipalType } from 'librechat-data-provider'; + +/** + * Normalizes a principalId to the correct type for MongoDB queries and storage. + * USER and GROUP principals are stored as ObjectIds; ROLE principals are strings. + * Ensures a string caller ID is cast to ObjectId so it matches documents written + * by `grantCapability` — which always stores user/group IDs as ObjectIds to match + * what `getUserPrincipals` returns. + */ +export const normalizePrincipalId = ( + principalId: string | Types.ObjectId, + principalType: PrincipalType, +): string | Types.ObjectId => { + if (typeof principalId === 'string' && principalType !== PrincipalType.ROLE) { + if (!Types.ObjectId.isValid(principalId)) { + throw new TypeError(`Invalid ObjectId string for ${principalType}: "${principalId}"`); + } + return new Types.ObjectId(principalId); + } + return principalId; +}; From e4e468840e0754bdcbb0b4242abbb32a4b533494 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 7 Mar 2026 16:37:10 -0500 Subject: [PATCH 098/111] =?UTF-8?q?=F0=9F=8F=A2=20feat:=20Multi-Tenant=20D?= =?UTF-8?q?ata=20Isolation=20Infrastructure=20(#12091)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: imports * chore: optional chaining in `spendTokens.spec.ts` * feat: Add tenantId field to all MongoDB schemas for multi-tenant isolation - Add AsyncLocalStorage-based tenant context (`tenantContext.ts`) for request-scoped tenantId propagation without modifying method signatures - Add Mongoose `applyTenantIsolation` plugin that injects `{ tenantId }` into all query filters when tenant context is present, with `TENANT_ISOLATION_STRICT` env var for fail-closed production mode - Add optional `tenantId` field to all 28 collection schemas - Update all compound unique indexes to include tenantId (email, OAuth IDs, role names, serverName, conversationId+user, messageId+user, etc.) - Apply tenant isolation plugin in all 28 model factories - Add `tenantId?: string` to all TypeScript document interfaces Behaviorally inert — transitional mode (default) passes through all queries unchanged. No migration required for existing deployments. * refactor: Update tenant context and enhance tenant isolation plugin - Changed `tenantId` in `TenantContext` to be optional, allowing for more flexible usage. - Refactored `runAsSystem` function to accept synchronous functions, improving usability. - Introduced comprehensive tests for the `applyTenantIsolation` plugin, ensuring correct tenant filtering in various query scenarios. - Enhanced the plugin to handle aggregate queries and save operations with tenant context, improving data isolation capabilities. * docs: tenant context documentation and improve tenant isolation tests - Added detailed documentation for the `tenantStorage` AsyncLocalStorage instance in `tenantContext.ts`, clarifying its usage for async tenant context propagation. - Updated tests in `tenantIsolation.spec.ts` to improve clarity and coverage, including new tests for strict mode behavior and tenant context propagation through await boundaries. - Refactored existing test cases for better readability and consistency, ensuring robust validation of tenant isolation functionality. * feat: Enhance tenant isolation by preventing tenantId mutations in update operations - Added a new function to assert that tenantId cannot be modified through update operators in Mongoose queries. - Implemented middleware to enforce this restriction during findOneAndUpdate, updateOne, and updateMany operations. - Updated documentation to reflect the new behavior regarding tenantId modifications, ensuring clarity on tenant isolation rules. * feat: Enhance tenant isolation tests and enforce tenantId restrictions - Updated existing tests to clarify behavior regarding tenantId preservation during save and insertMany operations. - Introduced new tests to validate that tenantId cannot be modified through update operations, ensuring strict adherence to tenant isolation rules. - Added checks for mismatched tenantId scenarios, reinforcing the integrity of tenant context propagation. - Enhanced test coverage for async context propagation and mutation guards, improving overall robustness of tenant isolation functionality. * fix: Remove duplicate re-exports in utils/index.ts Merge artifact caused `string` and `tempChatRetention` to be exported twice, which produces TypeScript compile errors for duplicate bindings. * fix: Resolve admin capability gap in multi-tenant mode (TODO #12091) - hasCapabilityForPrincipals now queries both tenant-scoped AND platform-level grants when tenantId is set, so seeded ADMIN grants remain effective in tenant mode. - Add applyTenantIsolation to SystemGrant model factory. * fix: Harden tenant isolation plugin - Add replaceGuard for replaceOne/findOneAndReplace to prevent cross-tenant document reassignment via replacement documents. - Cache isStrict() result to avoid process.env reads on every query. Export _resetStrictCache() for test teardown. - Replace console.warn with project logger (winston). - Add 5 new tests for replace guard behavior (46 total). * style: Fix import ordering in convo.ts and message.ts Move type imports after value imports per project style guide. * fix: Remove tenant isolation from SystemGrant, stamp tenantId in replaceGuard - SystemGrant is a cross-tenant control plane whose methods handle tenantId conditions explicitly. Applying the isolation plugin injects a hard equality filter that overrides the $and/$or logic in hasCapabilityForPrincipals, making platform-level ADMIN grants invisible in tenant mode. - replaceGuard now stamps tenantId into replacement documents when absent, preventing replaceOne from silently stripping tenant context. Replacements with a matching tenantId are allowed; mismatched tenantId still throws. * test: Add multi-tenant unique constraint and replace stamping tests - Verify same name/email can exist in different tenants (compound unique index allows it). - Verify duplicate within same tenant is rejected (E11000). - Verify tenant-scoped query returns only the correct document. - Update replaceOne test to assert tenantId is stamped into replacement document. - Add test for replacement with matching tenantId. * style: Reorder imports in message.ts to align with project style guide * feat: Add migration to drop superseded unique indexes for multi-tenancy Existing deployments have single-field unique indexes (e.g. { email: 1 }) that block multi-tenant operation — same email in different tenants triggers E11000. Mongoose autoIndex creates the new compound indexes but never drops the old ones. dropSupersededTenantIndexes() drops all 19 superseded indexes across 11 collections. It is idempotent, skips missing indexes/collections, and is a no-op on fresh databases. Must be called before enabling multi-tenant middleware on an existing deployment. Single-tenant deployments are unaffected (old indexes coexist harmlessly until migration runs). Includes 11 tests covering: - Full upgrade simulation (create old indexes, drop them, verify gone) - Multi-tenant writes work after migration (same email, different tenant) - Intra-tenant uniqueness preserved (duplicate within tenant rejected) - Fresh database (no-op, no errors) - Partial migration (some collections exist, some don't) - SUPERSEDED_INDEXES coverage validation * fix: Update systemGrant test — platform grants now satisfy tenant queries The TODO #12091 fix intentionally changed hasCapabilityForPrincipals to match both tenant-scoped AND platform-level grants. The test expected the old behavior (platform grant invisible to tenant query). Updated test name and expectation to match the new semantics. * fix: Align getCapabilitiesForPrincipal with hasCapabilityForPrincipals tenant query getCapabilitiesForPrincipal used a hard tenantId equality filter while hasCapabilityForPrincipals uses $and/$or to match both tenant-scoped and platform-level grants. This caused the two functions to disagree on what grants a principal holds in tenant mode. Apply the same $or pattern: when tenantId is provided, match both { tenantId } and { tenantId: { $exists: false } }. Adds test verifying platform-level ADMIN grants appear in getCapabilitiesForPrincipal when called with a tenantId. * fix: Remove categories from tenant index migration categoriesSchema is exported but never used to create a Mongoose model. No Category model factory exists, no code constructs a model from it, and no categories collection exists in production databases. Including it in the migration would attempt to drop indexes from a non-existent collection (harmlessly skipped) but implies the collection is managed. * fix: Restrict runAsSystem to async callbacks only Sync callbacks returning Mongoose thenables silently lose ALS context — the system bypass does nothing and strict mode throws with no indication runAsSystem was involved. Narrowing to () => Promise makes the wrong pattern a compile error. All existing call sites already use async. * fix: Use next(err) consistently in insertMany pre-hook The hook accepted a next callback but used throw for errors. Standardize on next(err) for all error paths so the hook speaks one language — callback-style throughout. * fix: Replace optional chaining with explicit null assertions in spendTokens tests Optional chaining on test assertions masks failures with unintelligible error messages. Add expect(result).not.toBeNull() before accessing properties, so a null result produces a clear diagnosis instead of "received value must be a number". --- .../data-schemas/src/config/tenantContext.ts | 28 + packages/data-schemas/src/index.ts | 3 + .../src/methods/spendTokens.spec.ts | 15 +- .../src/methods/systemGrant.spec.ts | 17 +- .../data-schemas/src/methods/systemGrant.ts | 12 +- packages/data-schemas/src/migrations/index.ts | 1 + .../src/migrations/tenantIndexes.spec.ts | 286 ++++++++ .../src/migrations/tenantIndexes.ts | 102 +++ .../data-schemas/src/models/accessRole.ts | 5 +- packages/data-schemas/src/models/aclEntry.ts | 5 +- packages/data-schemas/src/models/action.ts | 5 +- packages/data-schemas/src/models/agent.ts | 5 +- .../data-schemas/src/models/agentApiKey.ts | 2 + .../data-schemas/src/models/agentCategory.ts | 5 +- packages/data-schemas/src/models/assistant.ts | 5 +- packages/data-schemas/src/models/balance.ts | 5 +- packages/data-schemas/src/models/banner.ts | 5 +- .../src/models/conversationTag.ts | 5 +- packages/data-schemas/src/models/convo.ts | 5 +- packages/data-schemas/src/models/file.ts | 5 +- packages/data-schemas/src/models/group.ts | 5 +- packages/data-schemas/src/models/key.ts | 5 +- packages/data-schemas/src/models/mcpServer.ts | 5 +- packages/data-schemas/src/models/memory.ts | 2 + packages/data-schemas/src/models/message.ts | 5 +- .../data-schemas/src/models/pluginAuth.ts | 5 +- .../models/plugins/tenantIsolation.spec.ts | 660 ++++++++++++++++++ .../src/models/plugins/tenantIsolation.ts | 177 +++++ packages/data-schemas/src/models/preset.ts | 5 +- packages/data-schemas/src/models/prompt.ts | 5 +- .../data-schemas/src/models/promptGroup.ts | 5 +- packages/data-schemas/src/models/role.ts | 5 +- packages/data-schemas/src/models/session.ts | 5 +- .../data-schemas/src/models/sharedLink.ts | 5 +- .../data-schemas/src/models/systemGrant.ts | 7 +- packages/data-schemas/src/models/token.ts | 5 +- packages/data-schemas/src/models/toolCall.ts | 5 +- .../data-schemas/src/models/transaction.ts | 5 +- packages/data-schemas/src/models/user.ts | 5 +- .../data-schemas/src/schema/accessRole.ts | 7 +- packages/data-schemas/src/schema/aclEntry.ts | 16 +- packages/data-schemas/src/schema/action.ts | 4 + packages/data-schemas/src/schema/agent.ts | 4 + .../data-schemas/src/schema/agentApiKey.ts | 7 +- .../data-schemas/src/schema/agentCategory.ts | 6 +- packages/data-schemas/src/schema/assistant.ts | 4 + packages/data-schemas/src/schema/balance.ts | 4 + packages/data-schemas/src/schema/banner.ts | 5 + .../data-schemas/src/schema/categories.ts | 10 +- .../src/schema/conversationTag.ts | 7 +- packages/data-schemas/src/schema/convo.ts | 6 +- packages/data-schemas/src/schema/file.ts | 6 +- packages/data-schemas/src/schema/group.ts | 6 +- packages/data-schemas/src/schema/key.ts | 5 + packages/data-schemas/src/schema/mcpServer.ts | 6 +- packages/data-schemas/src/schema/memory.ts | 4 + packages/data-schemas/src/schema/message.ts | 6 +- .../data-schemas/src/schema/pluginAuth.ts | 4 + packages/data-schemas/src/schema/preset.ts | 5 + packages/data-schemas/src/schema/prompt.ts | 4 + .../data-schemas/src/schema/promptGroup.ts | 4 + packages/data-schemas/src/schema/role.ts | 8 +- packages/data-schemas/src/schema/session.ts | 4 + packages/data-schemas/src/schema/share.ts | 7 +- packages/data-schemas/src/schema/token.ts | 4 + packages/data-schemas/src/schema/toolCall.ts | 9 +- .../data-schemas/src/schema/transaction.ts | 5 + packages/data-schemas/src/schema/user.ts | 41 +- packages/data-schemas/src/types/accessRole.ts | 1 + packages/data-schemas/src/types/aclEntry.ts | 1 + packages/data-schemas/src/types/action.ts | 1 + packages/data-schemas/src/types/agent.ts | 1 + .../data-schemas/src/types/agentApiKey.ts | 1 + .../data-schemas/src/types/agentCategory.ts | 1 + packages/data-schemas/src/types/assistant.ts | 1 + packages/data-schemas/src/types/balance.ts | 1 + packages/data-schemas/src/types/banner.ts | 1 + packages/data-schemas/src/types/convo.ts | 1 + packages/data-schemas/src/types/file.ts | 1 + packages/data-schemas/src/types/group.ts | 1 + packages/data-schemas/src/types/mcp.ts | 1 + packages/data-schemas/src/types/memory.ts | 1 + packages/data-schemas/src/types/message.ts | 1 + packages/data-schemas/src/types/pluginAuth.ts | 1 + packages/data-schemas/src/types/prompts.ts | 2 + packages/data-schemas/src/types/role.ts | 1 + packages/data-schemas/src/types/session.ts | 1 + packages/data-schemas/src/types/token.ts | 1 + packages/data-schemas/src/types/user.ts | 1 + 89 files changed, 1539 insertions(+), 133 deletions(-) create mode 100644 packages/data-schemas/src/config/tenantContext.ts create mode 100644 packages/data-schemas/src/migrations/index.ts create mode 100644 packages/data-schemas/src/migrations/tenantIndexes.spec.ts create mode 100644 packages/data-schemas/src/migrations/tenantIndexes.ts create mode 100644 packages/data-schemas/src/models/plugins/tenantIsolation.spec.ts create mode 100644 packages/data-schemas/src/models/plugins/tenantIsolation.ts diff --git a/packages/data-schemas/src/config/tenantContext.ts b/packages/data-schemas/src/config/tenantContext.ts new file mode 100644 index 0000000000..e5e4376a90 --- /dev/null +++ b/packages/data-schemas/src/config/tenantContext.ts @@ -0,0 +1,28 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +export interface TenantContext { + tenantId?: string; +} + +/** Sentinel value for deliberate cross-tenant system operations */ +export const SYSTEM_TENANT_ID = '__SYSTEM__'; + +/** + * AsyncLocalStorage instance for propagating tenant context. + * Callbacks passed to `tenantStorage.run()` must be `async` for the context to propagate + * through Mongoose query execution. Sync callbacks returning a Mongoose thenable will lose context. + */ +export const tenantStorage = new AsyncLocalStorage(); + +/** Returns the current tenant ID from async context, or undefined if none is set */ +export function getTenantId(): string | undefined { + return tenantStorage.getStore()?.tenantId; +} + +/** + * Runs a function in an explicit cross-tenant system context (bypasses tenant filtering). + * The callback MUST be async — sync callbacks returning Mongoose thenables will lose context. + */ +export function runAsSystem(fn: () => Promise): Promise { + return tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, fn); +} diff --git a/packages/data-schemas/src/index.ts b/packages/data-schemas/src/index.ts index 3a34b574ae..485599c6f7 100644 --- a/packages/data-schemas/src/index.ts +++ b/packages/data-schemas/src/index.ts @@ -18,3 +18,6 @@ export type * from './types'; export type * from './methods'; export { default as logger } from './config/winston'; export { default as meiliLogger } from './config/meiliLogger'; +export { tenantStorage, getTenantId, runAsSystem, SYSTEM_TENANT_ID } from './config/tenantContext'; +export type { TenantContext } from './config/tenantContext'; +export { dropSupersededTenantIndexes } from './migrations'; diff --git a/packages/data-schemas/src/methods/spendTokens.spec.ts b/packages/data-schemas/src/methods/spendTokens.spec.ts index 58e5f4a0ab..5730bc7bdd 100644 --- a/packages/data-schemas/src/methods/spendTokens.spec.ts +++ b/packages/data-schemas/src/methods/spendTokens.spec.ts @@ -863,8 +863,9 @@ describe('spendTokens', () => { tokenUsage.promptTokens.read * (readRate ?? 0); const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; - expect(result?.prompt?.prompt).toBeCloseTo(-expectedPromptCost, 0); - expect(result?.completion?.completion).toBeCloseTo(-expectedCompletionCost, 0); + expect(result).not.toBeNull(); + expect(result!.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result!.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); }); it('should charge standard rates for structured tokens when below threshold', async () => { @@ -905,8 +906,9 @@ describe('spendTokens', () => { tokenUsage.promptTokens.read * (readRate ?? 0); const expectedCompletionCost = tokenUsage.completionTokens * standardCompletionRate; - expect(result?.prompt?.prompt).toBeCloseTo(-expectedPromptCost, 0); - expect(result?.completion?.completion).toBeCloseTo(-expectedCompletionCost, 0); + expect(result).not.toBeNull(); + expect(result!.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result!.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); }); it('should charge standard rates for gemini-3.1-pro-preview when prompt tokens are below threshold', async () => { @@ -1034,8 +1036,9 @@ describe('spendTokens', () => { tokenUsage.promptTokens.read * readRate; const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; - expect(result.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); - expect(result.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); + expect(result).not.toBeNull(); + expect(result!.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result!.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); }); it('should not apply premium pricing to non-premium models regardless of prompt size', async () => { diff --git a/packages/data-schemas/src/methods/systemGrant.spec.ts b/packages/data-schemas/src/methods/systemGrant.spec.ts index fb886c74d3..188d31b544 100644 --- a/packages/data-schemas/src/methods/systemGrant.spec.ts +++ b/packages/data-schemas/src/methods/systemGrant.spec.ts @@ -560,7 +560,7 @@ describe('systemGrant methods', () => { expect(result).toBe(false); }); - it('platform-level grant does not match tenant-scoped query', async () => { + it('platform-level grant satisfies tenant-scoped query', async () => { const userId = new Types.ObjectId(); await methods.grantCapability({ @@ -574,7 +574,7 @@ describe('systemGrant methods', () => { capability: SystemCapabilities.READ_CONFIGS, tenantId: 'tenant-1', }); - expect(result).toBe(false); + expect(result).toBe(true); }); it('tenant-scoped grant matches same-tenant query', async () => { @@ -679,6 +679,19 @@ describe('systemGrant methods', () => { expect(grants[0].capability).toBe(SystemCapabilities.READ_CONFIGS); }); + it('includes platform-level grants when called with a tenantId', async () => { + await methods.seedSystemGrants(); + + const grants = await methods.getCapabilitiesForPrincipal({ + principalType: PrincipalType.ROLE, + principalId: SystemRoles.ADMIN, + tenantId: 'acme', + }); + + expect(grants.some((g) => g.capability === SystemCapabilities.ACCESS_ADMIN)).toBe(true); + expect(grants).toHaveLength(Object.values(SystemCapabilities).length); + }); + it('throws TypeError for invalid ObjectId string on USER principal', async () => { await expect( methods.getCapabilitiesForPrincipal({ diff --git a/packages/data-schemas/src/methods/systemGrant.ts b/packages/data-schemas/src/methods/systemGrant.ts index f45d9fde9d..f0f389d762 100644 --- a/packages/data-schemas/src/methods/systemGrant.ts +++ b/packages/data-schemas/src/methods/systemGrant.ts @@ -54,16 +54,8 @@ export function createSystemGrantMethods(mongoose: typeof import('mongoose')) { capability: capabilityQuery, }; - /* - * TODO(#12091): In multi-tenant mode, platform-level grants (tenantId absent) - * should also satisfy tenant-scoped checks so that seeded ADMIN grants remain - * effective. When tenantId is set, query both tenant-scoped AND platform-level: - * query.$or = [{ tenantId }, { tenantId: { $exists: false } }] - * Also: getUserPrincipals currently has no tenantId param, so group memberships - * are returned across all tenants. Filter by tenant there too. - */ if (tenantId != null) { - query.tenantId = tenantId; + query.$and = [{ $or: [{ tenantId }, { tenantId: { $exists: false } }] }]; } else { query.tenantId = { $exists: false }; } @@ -194,7 +186,7 @@ export function createSystemGrantMethods(mongoose: typeof import('mongoose')) { }; if (tenantId != null) { - filter.tenantId = tenantId; + filter.$or = [{ tenantId }, { tenantId: { $exists: false } }]; } else { filter.tenantId = { $exists: false }; } diff --git a/packages/data-schemas/src/migrations/index.ts b/packages/data-schemas/src/migrations/index.ts new file mode 100644 index 0000000000..165b34dbf8 --- /dev/null +++ b/packages/data-schemas/src/migrations/index.ts @@ -0,0 +1 @@ +export { dropSupersededTenantIndexes } from './tenantIndexes'; diff --git a/packages/data-schemas/src/migrations/tenantIndexes.spec.ts b/packages/data-schemas/src/migrations/tenantIndexes.spec.ts new file mode 100644 index 0000000000..4637e7d0ad --- /dev/null +++ b/packages/data-schemas/src/migrations/tenantIndexes.spec.ts @@ -0,0 +1,286 @@ +import mongoose, { Schema } from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { dropSupersededTenantIndexes, SUPERSEDED_INDEXES } from './tenantIndexes'; + +jest.mock('~/config/winston', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +})); + +let mongoServer: InstanceType; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +describe('dropSupersededTenantIndexes', () => { + describe('with pre-existing single-field unique indexes (simulates upgrade)', () => { + beforeAll(async () => { + const db = mongoose.connection.db; + + await db.createCollection('users'); + const users = db.collection('users'); + await users.createIndex({ email: 1 }, { unique: true, name: 'email_1' }); + await users.createIndex({ googleId: 1 }, { unique: true, sparse: true, name: 'googleId_1' }); + await users.createIndex( + { facebookId: 1 }, + { unique: true, sparse: true, name: 'facebookId_1' }, + ); + await users.createIndex({ openidId: 1 }, { unique: true, sparse: true, name: 'openidId_1' }); + await users.createIndex({ samlId: 1 }, { unique: true, sparse: true, name: 'samlId_1' }); + await users.createIndex({ ldapId: 1 }, { unique: true, sparse: true, name: 'ldapId_1' }); + await users.createIndex({ githubId: 1 }, { unique: true, sparse: true, name: 'githubId_1' }); + await users.createIndex( + { discordId: 1 }, + { unique: true, sparse: true, name: 'discordId_1' }, + ); + await users.createIndex({ appleId: 1 }, { unique: true, sparse: true, name: 'appleId_1' }); + + await db.createCollection('roles'); + await db.collection('roles').createIndex({ name: 1 }, { unique: true, name: 'name_1' }); + + await db.createCollection('conversations'); + await db + .collection('conversations') + .createIndex( + { conversationId: 1, user: 1 }, + { unique: true, name: 'conversationId_1_user_1' }, + ); + + await db.createCollection('messages'); + await db + .collection('messages') + .createIndex({ messageId: 1, user: 1 }, { unique: true, name: 'messageId_1_user_1' }); + + await db.createCollection('agentcategories'); + await db + .collection('agentcategories') + .createIndex({ value: 1 }, { unique: true, name: 'value_1' }); + + await db.createCollection('accessroles'); + await db + .collection('accessroles') + .createIndex({ accessRoleId: 1 }, { unique: true, name: 'accessRoleId_1' }); + + await db.createCollection('conversationtags'); + await db + .collection('conversationtags') + .createIndex({ tag: 1, user: 1 }, { unique: true, name: 'tag_1_user_1' }); + + await db.createCollection('mcpservers'); + await db + .collection('mcpservers') + .createIndex({ serverName: 1 }, { unique: true, name: 'serverName_1' }); + + await db.createCollection('files'); + await db + .collection('files') + .createIndex( + { filename: 1, conversationId: 1, context: 1 }, + { unique: true, name: 'filename_1_conversationId_1_context_1' }, + ); + + await db.createCollection('groups'); + await db + .collection('groups') + .createIndex( + { idOnTheSource: 1, source: 1 }, + { unique: true, name: 'idOnTheSource_1_source_1' }, + ); + }); + + it('drops all superseded indexes', async () => { + const result = await dropSupersededTenantIndexes(mongoose.connection); + + expect(result.errors).toHaveLength(0); + expect(result.dropped.length).toBeGreaterThan(0); + + const totalExpected = Object.values(SUPERSEDED_INDEXES).reduce( + (sum, arr) => sum + arr.length, + 0, + ); + expect(result.dropped).toHaveLength(totalExpected); + }); + + it('reports no superseded indexes on second run (idempotent)', async () => { + const result = await dropSupersededTenantIndexes(mongoose.connection); + + expect(result.errors).toHaveLength(0); + expect(result.dropped).toHaveLength(0); + expect(result.skipped.length).toBeGreaterThan(0); + }); + + it('old unique indexes are actually gone from users collection', async () => { + const indexes = await mongoose.connection.db.collection('users').indexes(); + const indexNames = indexes.map((idx) => idx.name); + + expect(indexNames).not.toContain('email_1'); + expect(indexNames).not.toContain('googleId_1'); + expect(indexNames).not.toContain('openidId_1'); + expect(indexNames).toContain('_id_'); + }); + + it('old unique indexes are actually gone from roles collection', async () => { + const indexes = await mongoose.connection.db.collection('roles').indexes(); + const indexNames = indexes.map((idx) => idx.name); + + expect(indexNames).not.toContain('name_1'); + }); + + it('old compound unique indexes are gone from conversations collection', async () => { + const indexes = await mongoose.connection.db.collection('conversations').indexes(); + const indexNames = indexes.map((idx) => idx.name); + + expect(indexNames).not.toContain('conversationId_1_user_1'); + }); + }); + + describe('multi-tenant writes after migration', () => { + beforeAll(async () => { + const db = mongoose.connection.db; + + const users = db.collection('users'); + await users.createIndex( + { email: 1, tenantId: 1 }, + { unique: true, name: 'email_1_tenantId_1' }, + ); + }); + + it('allows same email in different tenants after old index is dropped', async () => { + const users = mongoose.connection.db.collection('users'); + + await users.insertOne({ + email: 'shared@example.com', + tenantId: 'tenant-a', + name: 'User A', + }); + await users.insertOne({ + email: 'shared@example.com', + tenantId: 'tenant-b', + name: 'User B', + }); + + const countA = await users.countDocuments({ + email: 'shared@example.com', + tenantId: 'tenant-a', + }); + const countB = await users.countDocuments({ + email: 'shared@example.com', + tenantId: 'tenant-b', + }); + + expect(countA).toBe(1); + expect(countB).toBe(1); + }); + + it('still rejects duplicate email within same tenant', async () => { + const users = mongoose.connection.db.collection('users'); + + await users.insertOne({ + email: 'unique-within@example.com', + tenantId: 'tenant-dup', + name: 'First', + }); + + await expect( + users.insertOne({ + email: 'unique-within@example.com', + tenantId: 'tenant-dup', + name: 'Second', + }), + ).rejects.toThrow(/E11000|duplicate key/); + }); + }); + + describe('on a fresh database (no pre-existing collections)', () => { + let freshServer: InstanceType; + let freshConnection: mongoose.Connection; + + beforeAll(async () => { + freshServer = await MongoMemoryServer.create(); + freshConnection = mongoose.createConnection(freshServer.getUri()); + await freshConnection.asPromise(); + }); + + afterAll(async () => { + await freshConnection.close(); + await freshServer.stop(); + }); + + it('skips all indexes gracefully (no errors)', async () => { + const result = await dropSupersededTenantIndexes(freshConnection); + + expect(result.errors).toHaveLength(0); + expect(result.dropped).toHaveLength(0); + expect(result.skipped.length).toBeGreaterThan(0); + }); + }); + + describe('partial migration (some indexes exist, some do not)', () => { + let partialServer: InstanceType; + let partialConnection: mongoose.Connection; + + beforeAll(async () => { + partialServer = await MongoMemoryServer.create(); + partialConnection = mongoose.createConnection(partialServer.getUri()); + await partialConnection.asPromise(); + + const db = partialConnection.db; + await db.createCollection('users'); + await db.collection('users').createIndex({ email: 1 }, { unique: true, name: 'email_1' }); + }); + + afterAll(async () => { + await partialConnection.close(); + await partialServer.stop(); + }); + + it('drops existing indexes and skips missing ones', async () => { + const result = await dropSupersededTenantIndexes(partialConnection); + + expect(result.errors).toHaveLength(0); + expect(result.dropped).toContain('users.email_1'); + expect(result.skipped.length).toBeGreaterThan(0); + + const skippedCollections = result.skipped.filter((s) => s.includes('does not exist')); + expect(skippedCollections.length).toBeGreaterThan(0); + }); + }); + + describe('SUPERSEDED_INDEXES coverage', () => { + it('covers all collections with unique index changes', () => { + const expectedCollections = [ + 'users', + 'roles', + 'conversations', + 'messages', + 'agentcategories', + 'accessroles', + 'conversationtags', + 'mcpservers', + 'files', + 'groups', + ]; + + for (const col of expectedCollections) { + expect(SUPERSEDED_INDEXES).toHaveProperty(col); + expect(SUPERSEDED_INDEXES[col].length).toBeGreaterThan(0); + } + }); + + it('users collection lists all 9 OAuth ID indexes plus email', () => { + expect(SUPERSEDED_INDEXES.users).toHaveLength(9); + expect(SUPERSEDED_INDEXES.users).toContain('email_1'); + expect(SUPERSEDED_INDEXES.users).toContain('googleId_1'); + expect(SUPERSEDED_INDEXES.users).toContain('openidId_1'); + }); + }); +}); diff --git a/packages/data-schemas/src/migrations/tenantIndexes.ts b/packages/data-schemas/src/migrations/tenantIndexes.ts new file mode 100644 index 0000000000..c68df4db2b --- /dev/null +++ b/packages/data-schemas/src/migrations/tenantIndexes.ts @@ -0,0 +1,102 @@ +import type { Connection } from 'mongoose'; +import logger from '~/config/winston'; + +/** + * Indexes that were superseded by compound tenant-scoped indexes. + * Each entry maps a collection name to the old index names that must be dropped + * before multi-tenancy can function (old unique indexes enforce global uniqueness, + * blocking same-value-different-tenant writes). + * + * These are only the indexes whose uniqueness constraints conflict with multi-tenancy. + * Non-unique indexes that were extended with tenantId are harmless (queries still work, + * just with slightly less optimal plans) and are not included here. + */ +const SUPERSEDED_INDEXES: Record = { + users: [ + 'email_1', + 'googleId_1', + 'facebookId_1', + 'openidId_1', + 'samlId_1', + 'ldapId_1', + 'githubId_1', + 'discordId_1', + 'appleId_1', + ], + roles: ['name_1'], + conversations: ['conversationId_1_user_1'], + messages: ['messageId_1_user_1'], + agentcategories: ['value_1'], + accessroles: ['accessRoleId_1'], + conversationtags: ['tag_1_user_1'], + mcpservers: ['serverName_1'], + files: ['filename_1_conversationId_1_context_1'], + groups: ['idOnTheSource_1_source_1'], +}; + +interface MigrationResult { + dropped: string[]; + skipped: string[]; + errors: string[]; +} + +/** + * Drops superseded unique indexes that block multi-tenant operation. + * Idempotent — skips indexes that don't exist. Safe to run on fresh databases. + * + * Call this before enabling multi-tenant middleware on an existing deployment. + * On a fresh database (no pre-existing data), this is a no-op. + */ +export async function dropSupersededTenantIndexes( + connection: Connection, +): Promise { + const result: MigrationResult = { dropped: [], skipped: [], errors: [] }; + + for (const [collectionName, indexNames] of Object.entries(SUPERSEDED_INDEXES)) { + const collection = connection.db.collection(collectionName); + + let existingIndexes: Array<{ name?: string }>; + try { + existingIndexes = await collection.indexes(); + } catch { + result.skipped.push( + ...indexNames.map((idx) => `${collectionName}.${idx} (collection does not exist)`), + ); + continue; + } + + const existingNames = new Set(existingIndexes.map((idx) => idx.name)); + + for (const indexName of indexNames) { + if (!existingNames.has(indexName)) { + result.skipped.push(`${collectionName}.${indexName}`); + continue; + } + + try { + await collection.dropIndex(indexName); + result.dropped.push(`${collectionName}.${indexName}`); + logger.info(`[TenantMigration] Dropped superseded index: ${collectionName}.${indexName}`); + } catch (err) { + const msg = `${collectionName}.${indexName}: ${(err as Error).message}`; + result.errors.push(msg); + logger.error(`[TenantMigration] Failed to drop index: ${msg}`); + } + } + } + + if (result.dropped.length > 0) { + logger.info( + `[TenantMigration] Migration complete. Dropped ${result.dropped.length} superseded indexes.`, + ); + } else { + logger.info( + '[TenantMigration] No superseded indexes found — database is already migrated or fresh.', + ); + } + + return result; +} + +/** Exported for testing — the raw index map */ +export { SUPERSEDED_INDEXES }; diff --git a/packages/data-schemas/src/models/accessRole.ts b/packages/data-schemas/src/models/accessRole.ts index 5da1e41dda..cd2c8b691c 100644 --- a/packages/data-schemas/src/models/accessRole.ts +++ b/packages/data-schemas/src/models/accessRole.ts @@ -1,10 +1,9 @@ import accessRoleSchema from '~/schema/accessRole'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the AccessRole model using the provided mongoose instance and schema - */ export function createAccessRoleModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(accessRoleSchema); return ( mongoose.models.AccessRole || mongoose.model('AccessRole', accessRoleSchema) ); diff --git a/packages/data-schemas/src/models/aclEntry.ts b/packages/data-schemas/src/models/aclEntry.ts index 62028d2a78..195b328438 100644 --- a/packages/data-schemas/src/models/aclEntry.ts +++ b/packages/data-schemas/src/models/aclEntry.ts @@ -1,9 +1,8 @@ import aclEntrySchema from '~/schema/aclEntry'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the AclEntry model using the provided mongoose instance and schema - */ export function createAclEntryModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(aclEntrySchema); return mongoose.models.AclEntry || mongoose.model('AclEntry', aclEntrySchema); } diff --git a/packages/data-schemas/src/models/action.ts b/packages/data-schemas/src/models/action.ts index 4778222460..421610ab73 100644 --- a/packages/data-schemas/src/models/action.ts +++ b/packages/data-schemas/src/models/action.ts @@ -1,9 +1,8 @@ import actionSchema from '~/schema/action'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IAction } from '~/types'; -/** - * Creates or returns the Action model using the provided mongoose instance and schema - */ export function createActionModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(actionSchema); return mongoose.models.Action || mongoose.model('Action', actionSchema); } diff --git a/packages/data-schemas/src/models/agent.ts b/packages/data-schemas/src/models/agent.ts index bff6bae60d..595d890f06 100644 --- a/packages/data-schemas/src/models/agent.ts +++ b/packages/data-schemas/src/models/agent.ts @@ -1,9 +1,8 @@ import agentSchema from '~/schema/agent'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IAgent } from '~/types'; -/** - * Creates or returns the Agent model using the provided mongoose instance and schema - */ export function createAgentModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(agentSchema); return mongoose.models.Agent || mongoose.model('Agent', agentSchema); } diff --git a/packages/data-schemas/src/models/agentApiKey.ts b/packages/data-schemas/src/models/agentApiKey.ts index 70387a2cef..8251b3d7cd 100644 --- a/packages/data-schemas/src/models/agentApiKey.ts +++ b/packages/data-schemas/src/models/agentApiKey.ts @@ -1,6 +1,8 @@ import agentApiKeySchema, { IAgentApiKey } from '~/schema/agentApiKey'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; export function createAgentApiKeyModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(agentApiKeySchema); return ( mongoose.models.AgentApiKey || mongoose.model('AgentApiKey', agentApiKeySchema) ); diff --git a/packages/data-schemas/src/models/agentCategory.ts b/packages/data-schemas/src/models/agentCategory.ts index 387e0b9e43..f0f1f79d2e 100644 --- a/packages/data-schemas/src/models/agentCategory.ts +++ b/packages/data-schemas/src/models/agentCategory.ts @@ -1,10 +1,9 @@ import agentCategorySchema from '~/schema/agentCategory'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the AgentCategory model using the provided mongoose instance and schema - */ export function createAgentCategoryModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(agentCategorySchema); return ( mongoose.models.AgentCategory || mongoose.model('AgentCategory', agentCategorySchema) diff --git a/packages/data-schemas/src/models/assistant.ts b/packages/data-schemas/src/models/assistant.ts index bf8a9f5ac7..16c6dc2bc6 100644 --- a/packages/data-schemas/src/models/assistant.ts +++ b/packages/data-schemas/src/models/assistant.ts @@ -1,9 +1,8 @@ import assistantSchema from '~/schema/assistant'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IAssistant } from '~/types'; -/** - * Creates or returns the Assistant model using the provided mongoose instance and schema - */ export function createAssistantModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(assistantSchema); return mongoose.models.Assistant || mongoose.model('Assistant', assistantSchema); } diff --git a/packages/data-schemas/src/models/balance.ts b/packages/data-schemas/src/models/balance.ts index e7ace38937..7c1fb34478 100644 --- a/packages/data-schemas/src/models/balance.ts +++ b/packages/data-schemas/src/models/balance.ts @@ -1,9 +1,8 @@ import balanceSchema from '~/schema/balance'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the Balance model using the provided mongoose instance and schema - */ export function createBalanceModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(balanceSchema); return mongoose.models.Balance || mongoose.model('Balance', balanceSchema); } diff --git a/packages/data-schemas/src/models/banner.ts b/packages/data-schemas/src/models/banner.ts index 7be6e2e07b..ac18fa72b4 100644 --- a/packages/data-schemas/src/models/banner.ts +++ b/packages/data-schemas/src/models/banner.ts @@ -1,9 +1,8 @@ import bannerSchema from '~/schema/banner'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IBanner } from '~/types'; -/** - * Creates or returns the Banner model using the provided mongoose instance and schema - */ export function createBannerModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(bannerSchema); return mongoose.models.Banner || mongoose.model('Banner', bannerSchema); } diff --git a/packages/data-schemas/src/models/conversationTag.ts b/packages/data-schemas/src/models/conversationTag.ts index 902915a6cc..a2df6459cc 100644 --- a/packages/data-schemas/src/models/conversationTag.ts +++ b/packages/data-schemas/src/models/conversationTag.ts @@ -1,9 +1,8 @@ import conversationTagSchema, { IConversationTag } from '~/schema/conversationTag'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; -/** - * Creates or returns the ConversationTag model using the provided mongoose instance and schema - */ export function createConversationTagModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(conversationTagSchema); return ( mongoose.models.ConversationTag || mongoose.model('ConversationTag', conversationTagSchema) diff --git a/packages/data-schemas/src/models/convo.ts b/packages/data-schemas/src/models/convo.ts index da0a8c68cf..7cf504bf48 100644 --- a/packages/data-schemas/src/models/convo.ts +++ b/packages/data-schemas/src/models/convo.ts @@ -1,11 +1,10 @@ import type * as t from '~/types'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import mongoMeili from '~/models/plugins/mongoMeili'; import convoSchema from '~/schema/convo'; -/** - * Creates or returns the Conversation model using the provided mongoose instance and schema - */ export function createConversationModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(convoSchema); if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) { convoSchema.plugin(mongoMeili, { mongoose, diff --git a/packages/data-schemas/src/models/file.ts b/packages/data-schemas/src/models/file.ts index c12dbec140..da291a13e3 100644 --- a/packages/data-schemas/src/models/file.ts +++ b/packages/data-schemas/src/models/file.ts @@ -1,9 +1,8 @@ import fileSchema from '~/schema/file'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IMongoFile } from '~/types'; -/** - * Creates or returns the File model using the provided mongoose instance and schema - */ export function createFileModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(fileSchema); return mongoose.models.File || mongoose.model('File', fileSchema); } diff --git a/packages/data-schemas/src/models/group.ts b/packages/data-schemas/src/models/group.ts index c6ee8f9516..0396de83f1 100644 --- a/packages/data-schemas/src/models/group.ts +++ b/packages/data-schemas/src/models/group.ts @@ -1,9 +1,8 @@ import groupSchema from '~/schema/group'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the Group model using the provided mongoose instance and schema - */ export function createGroupModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(groupSchema); return mongoose.models.Group || mongoose.model('Group', groupSchema); } diff --git a/packages/data-schemas/src/models/key.ts b/packages/data-schemas/src/models/key.ts index 6e2ff70c92..d6534e870b 100644 --- a/packages/data-schemas/src/models/key.ts +++ b/packages/data-schemas/src/models/key.ts @@ -1,8 +1,7 @@ import keySchema, { IKey } from '~/schema/key'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; -/** - * Creates or returns the Key model using the provided mongoose instance and schema - */ export function createKeyModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(keySchema); return mongoose.models.Key || mongoose.model('Key', keySchema); } diff --git a/packages/data-schemas/src/models/mcpServer.ts b/packages/data-schemas/src/models/mcpServer.ts index e2ad054068..6b93754d1c 100644 --- a/packages/data-schemas/src/models/mcpServer.ts +++ b/packages/data-schemas/src/models/mcpServer.ts @@ -1,10 +1,9 @@ import mcpServerSchema from '~/schema/mcpServer'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { MCPServerDocument } from '~/types'; -/** - * Creates or returns the MCPServer model using the provided mongoose instance and schema - */ export function createMCPServerModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(mcpServerSchema); return ( mongoose.models.MCPServer || mongoose.model('MCPServer', mcpServerSchema) ); diff --git a/packages/data-schemas/src/models/memory.ts b/packages/data-schemas/src/models/memory.ts index fb970c04ce..ad2c8cf8dc 100644 --- a/packages/data-schemas/src/models/memory.ts +++ b/packages/data-schemas/src/models/memory.ts @@ -1,6 +1,8 @@ import memorySchema from '~/schema/memory'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IMemoryEntry } from '~/types/memory'; export function createMemoryModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(memorySchema); return mongoose.models.MemoryEntry || mongoose.model('MemoryEntry', memorySchema); } diff --git a/packages/data-schemas/src/models/message.ts b/packages/data-schemas/src/models/message.ts index 3a81211e68..b8b26b3e06 100644 --- a/packages/data-schemas/src/models/message.ts +++ b/packages/data-schemas/src/models/message.ts @@ -1,11 +1,10 @@ import type * as t from '~/types'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import mongoMeili from '~/models/plugins/mongoMeili'; import messageSchema from '~/schema/message'; -/** - * Creates or returns the Message model using the provided mongoose instance and schema - */ export function createMessageModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(messageSchema); if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) { messageSchema.plugin(mongoMeili, { mongoose, diff --git a/packages/data-schemas/src/models/pluginAuth.ts b/packages/data-schemas/src/models/pluginAuth.ts index 5075fe6f43..22b46d05a8 100644 --- a/packages/data-schemas/src/models/pluginAuth.ts +++ b/packages/data-schemas/src/models/pluginAuth.ts @@ -1,9 +1,8 @@ import pluginAuthSchema from '~/schema/pluginAuth'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IPluginAuth } from '~/types/pluginAuth'; -/** - * Creates or returns the PluginAuth model using the provided mongoose instance and schema - */ export function createPluginAuthModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(pluginAuthSchema); return mongoose.models.PluginAuth || mongoose.model('PluginAuth', pluginAuthSchema); } diff --git a/packages/data-schemas/src/models/plugins/tenantIsolation.spec.ts b/packages/data-schemas/src/models/plugins/tenantIsolation.spec.ts new file mode 100644 index 0000000000..52c40c54bc --- /dev/null +++ b/packages/data-schemas/src/models/plugins/tenantIsolation.spec.ts @@ -0,0 +1,660 @@ +import mongoose, { Schema } from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { tenantStorage, runAsSystem, SYSTEM_TENANT_ID } from '~/config/tenantContext'; +import { applyTenantIsolation, _resetStrictCache } from './tenantIsolation'; + +let mongoServer: InstanceType; + +interface ITestDoc { + name: string; + tenantId?: string; +} + +function createTestModel(suffix: string) { + const schema = new Schema({ + name: { type: String, required: true }, + tenantId: { type: String, index: true }, + }); + applyTenantIsolation(schema); + const modelName = `TestTenant_${suffix}_${Date.now()}`; + return mongoose.model(modelName, schema); +} + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +describe('applyTenantIsolation', () => { + describe('idempotency', () => { + it('does not add duplicate hooks when called twice on the same schema', async () => { + const schema = new Schema({ + name: { type: String, required: true }, + tenantId: { type: String, index: true }, + }); + + applyTenantIsolation(schema); + applyTenantIsolation(schema); + + const modelName = `TestIdempotent_${Date.now()}`; + const Model = mongoose.model(modelName, schema); + + await Model.create([ + { name: 'a', tenantId: 'tenant-a' }, + { name: 'b', tenantId: 'tenant-b' }, + ]); + + const docs = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + Model.find().lean(), + ); + + expect(docs).toHaveLength(1); + expect(docs[0].name).toBe('a'); + }); + }); + + describe('query filtering', () => { + let TestModel: mongoose.Model; + + beforeAll(() => { + TestModel = createTestModel('query'); + }); + + beforeEach(async () => { + await TestModel.deleteMany({}); + await TestModel.create([ + { name: 'tenant-a-doc', tenantId: 'tenant-a' }, + { name: 'tenant-b-doc', tenantId: 'tenant-b' }, + { name: 'no-tenant-doc' }, + ]); + }); + + it('injects tenantId filter into find when context is set', async () => { + const docs = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.find().lean(), + ); + + expect(docs).toHaveLength(1); + expect(docs[0].name).toBe('tenant-a-doc'); + }); + + it('injects tenantId filter into findOne', async () => { + const doc = await tenantStorage.run({ tenantId: 'tenant-b' }, async () => + TestModel.findOne({ name: 'tenant-a-doc' }).lean(), + ); + + expect(doc).toBeNull(); + }); + + it('does not inject filter when context is absent (non-strict)', async () => { + const docs = await TestModel.find().lean(); + expect(docs).toHaveLength(3); + }); + + it('bypasses filter for SYSTEM_TENANT_ID', async () => { + const docs = await tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, async () => + TestModel.find().lean(), + ); + + expect(docs).toHaveLength(3); + }); + + it('injects tenantId filter into countDocuments', async () => { + const count = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.countDocuments(), + ); + + expect(count).toBe(1); + }); + + it('injects tenantId filter into findOneAndUpdate', async () => { + const doc = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.findOneAndUpdate( + { name: 'tenant-b-doc' }, + { $set: { name: 'updated' } }, + { new: true }, + ).lean(), + ); + + expect(doc).toBeNull(); + const original = await TestModel.findOne({ name: 'tenant-b-doc' }).lean(); + expect(original).not.toBeNull(); + }); + + it('injects tenantId filter into deleteOne', async () => { + await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.deleteOne({ name: 'tenant-b-doc' }), + ); + + const doc = await TestModel.findOne({ name: 'tenant-b-doc' }).lean(); + expect(doc).not.toBeNull(); + }); + + it('injects tenantId filter into deleteMany', async () => { + await tenantStorage.run({ tenantId: 'tenant-a' }, async () => TestModel.deleteMany({})); + + const remaining = await TestModel.find().lean(); + expect(remaining).toHaveLength(2); + expect(remaining.every((d) => d.tenantId !== 'tenant-a')).toBe(true); + }); + + it('injects tenantId filter into updateMany', async () => { + await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.updateMany({}, { $set: { name: 'updated' } }), + ); + + const tenantBDoc = await TestModel.findOne({ tenantId: 'tenant-b' }).lean(); + expect(tenantBDoc!.name).toBe('tenant-b-doc'); + + const tenantADoc = await TestModel.findOne({ tenantId: 'tenant-a' }).lean(); + expect(tenantADoc!.name).toBe('updated'); + }); + }); + + describe('aggregate filtering', () => { + let TestModel: mongoose.Model; + + beforeAll(() => { + TestModel = createTestModel('aggregate'); + }); + + beforeEach(async () => { + await TestModel.deleteMany({}); + await TestModel.create([ + { name: 'agg-a', tenantId: 'tenant-a' }, + { name: 'agg-b', tenantId: 'tenant-b' }, + { name: 'agg-none' }, + ]); + }); + + it('prepends $match stage with tenantId to aggregate pipeline', async () => { + const results = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.aggregate([{ $project: { name: 1 } }]), + ); + + expect(results).toHaveLength(1); + expect(results[0].name).toBe('agg-a'); + }); + + it('does not filter aggregate when no context is set (non-strict)', async () => { + const results = await TestModel.aggregate([{ $project: { name: 1 } }]); + expect(results).toHaveLength(3); + }); + + it('bypasses aggregate filter for SYSTEM_TENANT_ID', async () => { + const results = await tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, async () => + TestModel.aggregate([{ $project: { name: 1 } }]), + ); + + expect(results).toHaveLength(3); + }); + }); + + describe('save hook', () => { + let TestModel: mongoose.Model; + + beforeAll(() => { + TestModel = createTestModel('save'); + }); + + beforeEach(async () => { + await TestModel.deleteMany({}); + }); + + it('stamps tenantId on save for new documents', async () => { + const doc = await tenantStorage.run({ tenantId: 'tenant-x' }, async () => { + const d = new TestModel({ name: 'new-doc' }); + await d.save(); + return d; + }); + + expect(doc.tenantId).toBe('tenant-x'); + }); + + it('does not overwrite existing tenantId on save when it matches context', async () => { + const doc = await tenantStorage.run({ tenantId: 'tenant-x' }, async () => { + const d = new TestModel({ name: 'existing', tenantId: 'tenant-x' }); + await d.save(); + return d; + }); + + expect(doc.tenantId).toBe('tenant-x'); + }); + + it('allows mismatched tenantId on save in non-strict mode', async () => { + const doc = await tenantStorage.run({ tenantId: 'tenant-x' }, async () => { + const d = new TestModel({ name: 'mismatch', tenantId: 'tenant-other' }); + await d.save(); + return d; + }); + + expect(doc.tenantId).toBe('tenant-other'); + }); + + it('does not set tenantId for SYSTEM_TENANT_ID', async () => { + const doc = await tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, async () => { + const d = new TestModel({ name: 'system-doc' }); + await d.save(); + return d; + }); + + expect(doc.tenantId).toBeUndefined(); + }); + + it('saves without tenantId when no context is set (non-strict)', async () => { + const doc = new TestModel({ name: 'no-context' }); + await doc.save(); + + expect(doc.tenantId).toBeUndefined(); + }); + }); + + describe('insertMany hook', () => { + let TestModel: mongoose.Model; + + beforeAll(() => { + TestModel = createTestModel('insertMany'); + }); + + beforeEach(async () => { + await TestModel.deleteMany({}); + }); + + it('stamps tenantId on all insertMany docs', async () => { + const docs = await tenantStorage.run({ tenantId: 'tenant-bulk' }, async () => + TestModel.insertMany([{ name: 'bulk-1' }, { name: 'bulk-2' }]), + ); + + expect(docs).toHaveLength(2); + for (const doc of docs) { + expect(doc.tenantId).toBe('tenant-bulk'); + } + }); + + it('does not overwrite existing tenantId in insertMany when it matches', async () => { + const docs = await tenantStorage.run({ tenantId: 'tenant-bulk' }, async () => + TestModel.insertMany([{ name: 'pre-set', tenantId: 'tenant-bulk' }]), + ); + + expect(docs[0].tenantId).toBe('tenant-bulk'); + }); + + it('allows mismatched tenantId in insertMany in non-strict mode', async () => { + const docs = await tenantStorage.run({ tenantId: 'tenant-bulk' }, async () => + TestModel.insertMany([{ name: 'mismatch', tenantId: 'tenant-other' }]), + ); + + expect(docs[0].tenantId).toBe('tenant-other'); + }); + + it('does not hang when no tenant context is set (non-strict)', async () => { + const docs = await TestModel.insertMany([{ name: 'no-context-bulk' }]); + + expect(docs).toHaveLength(1); + expect(docs[0].tenantId).toBeUndefined(); + }); + + it('does not stamp tenantId for SYSTEM_TENANT_ID', async () => { + const docs = await tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, async () => + TestModel.insertMany([{ name: 'system-bulk' }]), + ); + + expect(docs[0].tenantId).toBeUndefined(); + }); + }); + + describe('update mutation guard', () => { + let TestModel: mongoose.Model; + + beforeAll(() => { + TestModel = createTestModel('mutation'); + }); + + beforeEach(async () => { + await TestModel.deleteMany({}); + await TestModel.create({ name: 'guarded', tenantId: 'tenant-a' }); + }); + + it('blocks $set of tenantId via findOneAndUpdate', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.findOneAndUpdate({ name: 'guarded' }, { $set: { tenantId: 'tenant-b' } }), + ), + ).rejects.toThrow('[TenantIsolation] Modifying tenantId via update operators is not allowed'); + }); + + it('blocks $unset of tenantId via updateOne', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.updateOne({ name: 'guarded' }, { $unset: { tenantId: 1 } }), + ), + ).rejects.toThrow('[TenantIsolation] Modifying tenantId via update operators is not allowed'); + }); + + it('blocks top-level tenantId in update payload', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.updateOne({ name: 'guarded' }, { tenantId: 'tenant-b' } as Record< + string, + string + >), + ), + ).rejects.toThrow('[TenantIsolation] Modifying tenantId via update operators is not allowed'); + }); + + it('blocks $setOnInsert of tenantId via updateMany', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.updateMany({}, { $setOnInsert: { tenantId: 'tenant-b' } }), + ), + ).rejects.toThrow('[TenantIsolation] Modifying tenantId via update operators is not allowed'); + }); + + it('allows updates that do not touch tenantId', async () => { + const result = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.findOneAndUpdate( + { name: 'guarded' }, + { $set: { name: 'updated' } }, + { new: true }, + ).lean(), + ); + + expect(result).not.toBeNull(); + expect(result!.name).toBe('updated'); + expect(result!.tenantId).toBe('tenant-a'); + }); + + it('allows SYSTEM_TENANT_ID to modify tenantId', async () => { + const result = await tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, async () => + TestModel.findOneAndUpdate( + { name: 'guarded' }, + { $set: { tenantId: 'tenant-b' } }, + { new: true }, + ).lean(), + ); + + expect(result).not.toBeNull(); + expect(result!.tenantId).toBe('tenant-b'); + }); + + it('blocks tenantId mutation even without tenant context', async () => { + await expect( + TestModel.updateOne({ name: 'guarded' }, { $set: { tenantId: 'tenant-b' } }), + ).rejects.toThrow('[TenantIsolation] Modifying tenantId via update operators is not allowed'); + }); + + it('blocks tenantId in replaceOne replacement document', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.replaceOne({ name: 'guarded' }, { name: 'replaced', tenantId: 'tenant-b' }), + ), + ).rejects.toThrow('[TenantIsolation] Modifying tenantId via replacement is not allowed'); + }); + + it('blocks tenantId in findOneAndReplace replacement document', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.findOneAndReplace( + { name: 'guarded' }, + { name: 'replaced', tenantId: 'tenant-b' }, + ), + ), + ).rejects.toThrow('[TenantIsolation] Modifying tenantId via replacement is not allowed'); + }); + + it('stamps tenantId into replacement when absent from replacement document', async () => { + await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.replaceOne({ name: 'guarded' }, { name: 'replaced-ok' }), + ); + + const doc = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.findOne({ name: 'replaced-ok' }).lean(), + ); + expect(doc).not.toBeNull(); + expect(doc!.tenantId).toBe('tenant-a'); + }); + + it('allows replacement with matching tenantId', async () => { + await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.replaceOne({ name: 'guarded' }, { name: 'replaced-match', tenantId: 'tenant-a' }), + ); + + const doc = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.findOne({ name: 'replaced-match' }).lean(), + ); + expect(doc).not.toBeNull(); + expect(doc!.tenantId).toBe('tenant-a'); + }); + + it('allows SYSTEM_TENANT_ID to replace with tenantId', async () => { + await tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, async () => + TestModel.replaceOne({ name: 'guarded' }, { name: 'sys-replaced', tenantId: 'tenant-b' }), + ); + + const doc = await TestModel.findOne({ name: 'sys-replaced' }).lean(); + expect(doc).not.toBeNull(); + expect(doc!.tenantId).toBe('tenant-b'); + }); + }); + + describe('runAsSystem', () => { + let TestModel: mongoose.Model; + + beforeAll(() => { + TestModel = createTestModel('runAsSystem'); + }); + + beforeEach(async () => { + await TestModel.deleteMany({}); + await TestModel.create([ + { name: 'sys-a', tenantId: 'tenant-a' }, + { name: 'sys-b', tenantId: 'tenant-b' }, + ]); + }); + + it('bypasses tenant filter inside runAsSystem', async () => { + const docs = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + runAsSystem(async () => TestModel.find().lean()), + ); + + expect(docs).toHaveLength(2); + }); + }); + + describe('async context propagation', () => { + let TestModel: mongoose.Model; + + beforeAll(() => { + TestModel = createTestModel('asyncCtx'); + }); + + beforeEach(async () => { + await TestModel.deleteMany({}); + await TestModel.create([ + { name: 'ctx-a', tenantId: 'tenant-a' }, + { name: 'ctx-b', tenantId: 'tenant-b' }, + ]); + }); + + it('propagates tenant context through await boundaries', async () => { + const docs = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return TestModel.find().lean(); + }); + + expect(docs).toHaveLength(1); + expect(docs[0].name).toBe('ctx-a'); + }); + }); + + describe('strict mode', () => { + let TestModel: mongoose.Model; + const originalEnv = process.env.TENANT_ISOLATION_STRICT; + + beforeAll(() => { + TestModel = createTestModel('strict'); + }); + + beforeEach(async () => { + await runAsSystem(async () => { + await TestModel.deleteMany({}); + await TestModel.create({ name: 'strict-doc', tenantId: 'tenant-a' }); + }); + process.env.TENANT_ISOLATION_STRICT = 'true'; + _resetStrictCache(); + }); + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.TENANT_ISOLATION_STRICT; + } else { + process.env.TENANT_ISOLATION_STRICT = originalEnv; + } + _resetStrictCache(); + }); + + it('throws on find without tenant context', async () => { + await expect(TestModel.find().lean()).rejects.toThrow( + '[TenantIsolation] Query attempted without tenant context in strict mode', + ); + }); + + it('throws on findOne without tenant context', async () => { + await expect(TestModel.findOne().lean()).rejects.toThrow('[TenantIsolation]'); + }); + + it('throws on aggregate without tenant context', async () => { + await expect(TestModel.aggregate([{ $project: { name: 1 } }])).rejects.toThrow( + '[TenantIsolation] Aggregate attempted without tenant context in strict mode', + ); + }); + + it('throws on save without tenant context', async () => { + const doc = new TestModel({ name: 'strict-new' }); + await expect(doc.save()).rejects.toThrow( + '[TenantIsolation] Save attempted without tenant context in strict mode', + ); + }); + + it('throws on insertMany without tenant context', async () => { + await expect(TestModel.insertMany([{ name: 'strict-bulk' }])).rejects.toThrow( + '[TenantIsolation] insertMany attempted without tenant context in strict mode', + ); + }); + + it('throws on save with mismatched tenantId', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => { + const d = new TestModel({ name: 'mismatch', tenantId: 'tenant-b' }); + await d.save(); + }), + ).rejects.toThrow( + '[TenantIsolation] Document tenantId does not match current tenant context', + ); + }); + + it('throws on insertMany with mismatched tenantId', async () => { + await expect( + tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.insertMany([{ name: 'mismatch', tenantId: 'tenant-b' }]), + ), + ).rejects.toThrow( + '[TenantIsolation] Document tenantId does not match current tenant context', + ); + }); + + it('allows queries with tenant context in strict mode', async () => { + const docs = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + TestModel.find().lean(), + ); + + expect(docs).toHaveLength(1); + }); + + it('allows SYSTEM_TENANT_ID to bypass strict mode', async () => { + const docs = await tenantStorage.run({ tenantId: SYSTEM_TENANT_ID }, async () => + TestModel.find().lean(), + ); + + expect(docs).toHaveLength(1); + }); + + it('allows runAsSystem to bypass strict mode', async () => { + const docs = await runAsSystem(async () => TestModel.find().lean()); + expect(docs).toHaveLength(1); + }); + }); + + describe('multi-tenant unique constraints', () => { + let UniqueModel: mongoose.Model; + + beforeAll(async () => { + const schema = new Schema({ + name: { type: String, required: true }, + tenantId: { type: String, index: true }, + }); + schema.index({ name: 1, tenantId: 1 }, { unique: true }); + applyTenantIsolation(schema); + UniqueModel = mongoose.model(`TestUnique_${Date.now()}`, schema); + await UniqueModel.ensureIndexes(); + }); + + afterAll(async () => { + await UniqueModel.deleteMany({}); + }); + + it('allows same name in different tenants', async () => { + await tenantStorage.run({ tenantId: 'tenant-a' }, async () => { + await UniqueModel.create({ name: 'shared-name' }); + }); + await tenantStorage.run({ tenantId: 'tenant-b' }, async () => { + await UniqueModel.create({ name: 'shared-name' }); + }); + + const docA = await tenantStorage.run({ tenantId: 'tenant-a' }, async () => + UniqueModel.findOne({ name: 'shared-name' }).lean(), + ); + const docB = await tenantStorage.run({ tenantId: 'tenant-b' }, async () => + UniqueModel.findOne({ name: 'shared-name' }).lean(), + ); + + expect(docA).not.toBeNull(); + expect(docB).not.toBeNull(); + expect(docA!.tenantId).toBe('tenant-a'); + expect(docB!.tenantId).toBe('tenant-b'); + }); + + it('rejects duplicate name within the same tenant', async () => { + await tenantStorage.run({ tenantId: 'tenant-dup' }, async () => { + await UniqueModel.create({ name: 'unique-within-tenant' }); + }); + + await expect( + tenantStorage.run({ tenantId: 'tenant-dup' }, async () => + UniqueModel.create({ name: 'unique-within-tenant' }), + ), + ).rejects.toThrow(/E11000|duplicate key/); + }); + + it('tenant-scoped query returns only the correct document', async () => { + await tenantStorage.run({ tenantId: 'tenant-x' }, async () => { + await UniqueModel.create({ name: 'scoped-doc' }); + }); + await tenantStorage.run({ tenantId: 'tenant-y' }, async () => { + await UniqueModel.create({ name: 'scoped-doc' }); + }); + + const results = await tenantStorage.run({ tenantId: 'tenant-x' }, async () => + UniqueModel.find({ name: 'scoped-doc' }).lean(), + ); + + expect(results).toHaveLength(1); + expect(results[0].tenantId).toBe('tenant-x'); + }); + }); +}); diff --git a/packages/data-schemas/src/models/plugins/tenantIsolation.ts b/packages/data-schemas/src/models/plugins/tenantIsolation.ts new file mode 100644 index 0000000000..ddb98f9aa9 --- /dev/null +++ b/packages/data-schemas/src/models/plugins/tenantIsolation.ts @@ -0,0 +1,177 @@ +import type { Schema, Query, Aggregate, UpdateQuery } from 'mongoose'; +import { getTenantId, SYSTEM_TENANT_ID } from '~/config/tenantContext'; +import logger from '~/config/winston'; + +let _strictMode: boolean | undefined; + +function isStrict(): boolean { + return (_strictMode ??= process.env.TENANT_ISOLATION_STRICT === 'true'); +} + +/** Resets the cached strict-mode flag. Exposed for test teardown only. */ +export function _resetStrictCache(): void { + _strictMode = undefined; +} + +if ( + process.env.TENANT_ISOLATION_STRICT && + process.env.TENANT_ISOLATION_STRICT !== 'true' && + process.env.TENANT_ISOLATION_STRICT !== 'false' +) { + logger.warn( + `[TenantIsolation] TENANT_ISOLATION_STRICT="${process.env.TENANT_ISOLATION_STRICT}" ` + + 'is not "true" or "false"; defaulting to non-strict mode.', + ); +} + +const TENANT_ISOLATION_APPLIED = Symbol.for('librechat:tenantIsolation'); + +const MUTATION_OPERATORS = ['$set', '$unset', '$setOnInsert', '$rename'] as const; + +function assertNoTenantIdMutation(update: UpdateQuery | null): void { + if (!update) { + return; + } + for (const op of MUTATION_OPERATORS) { + const payload = update[op] as Record | undefined; + if (payload && 'tenantId' in payload) { + throw new Error('[TenantIsolation] Modifying tenantId via update operators is not allowed'); + } + } + if ('tenantId' in update) { + throw new Error('[TenantIsolation] Modifying tenantId via update operators is not allowed'); + } +} + +/** + * Mongoose schema plugin that enforces tenant-level data isolation. + * + * - `tenantId` present in async context -> injected into every query filter. + * - `tenantId` is `SYSTEM_TENANT_ID` -> skips injection (explicit cross-tenant op). + * - `tenantId` absent + `TENANT_ISOLATION_STRICT=true` -> throws (fail-closed). + * - `tenantId` absent + strict mode off -> passes through (transitional/pre-tenancy). + * - Update and replace operations that modify `tenantId` are blocked unless running as system. + */ +export function applyTenantIsolation(schema: Schema): void { + const s = schema as Schema & { [key: symbol]: boolean }; + if (s[TENANT_ISOLATION_APPLIED]) { + return; + } + s[TENANT_ISOLATION_APPLIED] = true; + + const queryMiddleware = function (this: Query) { + const tenantId = getTenantId(); + + if (!tenantId && isStrict()) { + throw new Error('[TenantIsolation] Query attempted without tenant context in strict mode'); + } + + if (!tenantId || tenantId === SYSTEM_TENANT_ID) { + return; + } + + this.where({ tenantId }); + }; + + const updateGuard = function (this: Query) { + const tenantId = getTenantId(); + if (tenantId === SYSTEM_TENANT_ID) { + return; + } + assertNoTenantIdMutation(this.getUpdate() as UpdateQuery | null); + }; + + const replaceGuard = function (this: Query) { + const tenantId = getTenantId(); + if (tenantId === SYSTEM_TENANT_ID) { + return; + } + const replacement = this.getUpdate() as Record | null; + if (!replacement) { + return; + } + if ('tenantId' in replacement && replacement.tenantId !== tenantId) { + throw new Error('[TenantIsolation] Modifying tenantId via replacement is not allowed'); + } + if (tenantId && !('tenantId' in replacement)) { + replacement.tenantId = tenantId; + } + }; + + schema.pre('find', queryMiddleware); + schema.pre('findOne', queryMiddleware); + schema.pre('findOneAndUpdate', queryMiddleware); + schema.pre('findOneAndDelete', queryMiddleware); + schema.pre('findOneAndReplace', queryMiddleware); + schema.pre('updateOne', queryMiddleware); + schema.pre('updateMany', queryMiddleware); + schema.pre('deleteOne', queryMiddleware); + schema.pre('deleteMany', queryMiddleware); + schema.pre('countDocuments', queryMiddleware); + schema.pre('replaceOne', queryMiddleware); + + schema.pre('findOneAndUpdate', updateGuard); + schema.pre('updateOne', updateGuard); + schema.pre('updateMany', updateGuard); + + schema.pre('replaceOne', replaceGuard); + schema.pre('findOneAndReplace', replaceGuard); + + schema.pre('aggregate', function (this: Aggregate) { + const tenantId = getTenantId(); + + if (!tenantId && isStrict()) { + throw new Error( + '[TenantIsolation] Aggregate attempted without tenant context in strict mode', + ); + } + + if (!tenantId || tenantId === SYSTEM_TENANT_ID) { + return; + } + + this.pipeline().unshift({ $match: { tenantId } }); + }); + + schema.pre('save', function () { + const tenantId = getTenantId(); + + if (!tenantId && isStrict()) { + throw new Error('[TenantIsolation] Save attempted without tenant context in strict mode'); + } + + if (tenantId && tenantId !== SYSTEM_TENANT_ID) { + if (!this.tenantId) { + this.tenantId = tenantId; + } else if (isStrict() && this.tenantId !== tenantId) { + throw new Error( + '[TenantIsolation] Document tenantId does not match current tenant context', + ); + } + } + }); + + schema.pre('insertMany', function (next, docs) { + const tenantId = getTenantId(); + + if (!tenantId && isStrict()) { + return next( + new Error('[TenantIsolation] insertMany attempted without tenant context in strict mode'), + ); + } + + if (tenantId && tenantId !== SYSTEM_TENANT_ID && Array.isArray(docs)) { + for (const doc of docs) { + if (!doc.tenantId) { + doc.tenantId = tenantId; + } else if (isStrict() && doc.tenantId !== tenantId) { + return next( + new Error('[TenantIsolation] Document tenantId does not match current tenant context'), + ); + } + } + } + + next(); + }); +} diff --git a/packages/data-schemas/src/models/preset.ts b/packages/data-schemas/src/models/preset.ts index c5b156e555..dc61c6d251 100644 --- a/packages/data-schemas/src/models/preset.ts +++ b/packages/data-schemas/src/models/preset.ts @@ -1,8 +1,7 @@ import presetSchema, { IPreset } from '~/schema/preset'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; -/** - * Creates or returns the Preset model using the provided mongoose instance and schema - */ export function createPresetModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(presetSchema); return mongoose.models.Preset || mongoose.model('Preset', presetSchema); } diff --git a/packages/data-schemas/src/models/prompt.ts b/packages/data-schemas/src/models/prompt.ts index 87edfa1ef8..25ff23e81a 100644 --- a/packages/data-schemas/src/models/prompt.ts +++ b/packages/data-schemas/src/models/prompt.ts @@ -1,9 +1,8 @@ import promptSchema from '~/schema/prompt'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IPrompt } from '~/types/prompts'; -/** - * Creates or returns the Prompt model using the provided mongoose instance and schema - */ export function createPromptModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(promptSchema); return mongoose.models.Prompt || mongoose.model('Prompt', promptSchema); } diff --git a/packages/data-schemas/src/models/promptGroup.ts b/packages/data-schemas/src/models/promptGroup.ts index 8de3dc9e16..2d1d226988 100644 --- a/packages/data-schemas/src/models/promptGroup.ts +++ b/packages/data-schemas/src/models/promptGroup.ts @@ -1,10 +1,9 @@ import promptGroupSchema from '~/schema/promptGroup'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IPromptGroupDocument } from '~/types/prompts'; -/** - * Creates or returns the PromptGroup model using the provided mongoose instance and schema - */ export function createPromptGroupModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(promptGroupSchema); return ( mongoose.models.PromptGroup || mongoose.model('PromptGroup', promptGroupSchema) diff --git a/packages/data-schemas/src/models/role.ts b/packages/data-schemas/src/models/role.ts index ccc56af1d6..2860007044 100644 --- a/packages/data-schemas/src/models/role.ts +++ b/packages/data-schemas/src/models/role.ts @@ -1,9 +1,8 @@ import roleSchema from '~/schema/role'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type { IRole } from '~/types'; -/** - * Creates or returns the Role model using the provided mongoose instance and schema - */ export function createRoleModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(roleSchema); return mongoose.models.Role || mongoose.model('Role', roleSchema); } diff --git a/packages/data-schemas/src/models/session.ts b/packages/data-schemas/src/models/session.ts index 3d4eba2761..746f9a74dd 100644 --- a/packages/data-schemas/src/models/session.ts +++ b/packages/data-schemas/src/models/session.ts @@ -1,9 +1,8 @@ import sessionSchema from '~/schema/session'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the Session model using the provided mongoose instance and schema - */ export function createSessionModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(sessionSchema); return mongoose.models.Session || mongoose.model('Session', sessionSchema); } diff --git a/packages/data-schemas/src/models/sharedLink.ts b/packages/data-schemas/src/models/sharedLink.ts index 662f9aafc4..f379c4605c 100644 --- a/packages/data-schemas/src/models/sharedLink.ts +++ b/packages/data-schemas/src/models/sharedLink.ts @@ -1,8 +1,7 @@ import shareSchema, { ISharedLink } from '~/schema/share'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; -/** - * Creates or returns the SharedLink model using the provided mongoose instance and schema - */ export function createSharedLinkModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(shareSchema); return mongoose.models.SharedLink || mongoose.model('SharedLink', shareSchema); } diff --git a/packages/data-schemas/src/models/systemGrant.ts b/packages/data-schemas/src/models/systemGrant.ts index e439d2af81..e30b444c65 100644 --- a/packages/data-schemas/src/models/systemGrant.ts +++ b/packages/data-schemas/src/models/systemGrant.ts @@ -1,8 +1,11 @@ -import systemGrantSchema from '~/schema/systemGrant'; import type * as t from '~/types'; +import systemGrantSchema from '~/schema/systemGrant'; /** - * Creates or returns the SystemGrant model using the provided mongoose instance and schema + * SystemGrant is a cross-tenant control plane — its query logic in systemGrant methods + * explicitly handles tenantId conditions (platform-level vs tenant-scoped grants). + * Do NOT apply tenant isolation plugin here; it would inject a hard tenantId equality + * filter that conflicts with the $and/$or logic in hasCapabilityForPrincipals. */ export function createSystemGrantModel(mongoose: typeof import('mongoose')) { return ( diff --git a/packages/data-schemas/src/models/token.ts b/packages/data-schemas/src/models/token.ts index 870233f615..0cdefab0d9 100644 --- a/packages/data-schemas/src/models/token.ts +++ b/packages/data-schemas/src/models/token.ts @@ -1,9 +1,8 @@ import tokenSchema from '~/schema/token'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the Token model using the provided mongoose instance and schema - */ export function createTokenModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(tokenSchema); return mongoose.models.Token || mongoose.model('Token', tokenSchema); } diff --git a/packages/data-schemas/src/models/toolCall.ts b/packages/data-schemas/src/models/toolCall.ts index 18292fd8e8..262aade342 100644 --- a/packages/data-schemas/src/models/toolCall.ts +++ b/packages/data-schemas/src/models/toolCall.ts @@ -1,8 +1,7 @@ import toolCallSchema, { IToolCallData } from '~/schema/toolCall'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; -/** - * Creates or returns the ToolCall model using the provided mongoose instance and schema - */ export function createToolCallModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(toolCallSchema); return mongoose.models.ToolCall || mongoose.model('ToolCall', toolCallSchema); } diff --git a/packages/data-schemas/src/models/transaction.ts b/packages/data-schemas/src/models/transaction.ts index 52a33b86a7..358ead23a7 100644 --- a/packages/data-schemas/src/models/transaction.ts +++ b/packages/data-schemas/src/models/transaction.ts @@ -1,9 +1,8 @@ import transactionSchema, { ITransaction } from '~/schema/transaction'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; -/** - * Creates or returns the Transaction model using the provided mongoose instance and schema - */ export function createTransactionModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(transactionSchema); return ( mongoose.models.Transaction || mongoose.model('Transaction', transactionSchema) ); diff --git a/packages/data-schemas/src/models/user.ts b/packages/data-schemas/src/models/user.ts index 1aef66f6d1..18a1da2b0b 100644 --- a/packages/data-schemas/src/models/user.ts +++ b/packages/data-schemas/src/models/user.ts @@ -1,9 +1,8 @@ import userSchema from '~/schema/user'; +import { applyTenantIsolation } from '~/models/plugins/tenantIsolation'; import type * as t from '~/types'; -/** - * Creates or returns the User model using the provided mongoose instance and schema - */ export function createUserModel(mongoose: typeof import('mongoose')) { + applyTenantIsolation(userSchema); return mongoose.models.User || mongoose.model('User', userSchema); } diff --git a/packages/data-schemas/src/schema/accessRole.ts b/packages/data-schemas/src/schema/accessRole.ts index 52f9e796c0..dbcaf83ddb 100644 --- a/packages/data-schemas/src/schema/accessRole.ts +++ b/packages/data-schemas/src/schema/accessRole.ts @@ -7,7 +7,6 @@ const accessRoleSchema = new Schema( type: String, required: true, index: true, - unique: true, }, name: { type: String, @@ -24,8 +23,14 @@ const accessRoleSchema = new Schema( type: Number, required: true, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); +accessRoleSchema.index({ accessRoleId: 1, tenantId: 1 }, { unique: true }); + export default accessRoleSchema; diff --git a/packages/data-schemas/src/schema/aclEntry.ts b/packages/data-schemas/src/schema/aclEntry.ts index dbaf73b466..e58cb1a424 100644 --- a/packages/data-schemas/src/schema/aclEntry.ts +++ b/packages/data-schemas/src/schema/aclEntry.ts @@ -55,12 +55,22 @@ const aclEntrySchema = new Schema( type: Date, default: Date.now, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); -aclEntrySchema.index({ principalId: 1, principalType: 1, resourceType: 1, resourceId: 1 }); -aclEntrySchema.index({ resourceId: 1, principalType: 1, principalId: 1 }); -aclEntrySchema.index({ principalId: 1, permBits: 1, resourceType: 1 }); +aclEntrySchema.index({ + principalId: 1, + principalType: 1, + resourceType: 1, + resourceId: 1, + tenantId: 1, +}); +aclEntrySchema.index({ resourceId: 1, principalType: 1, principalId: 1, tenantId: 1 }); +aclEntrySchema.index({ principalId: 1, permBits: 1, resourceType: 1, tenantId: 1 }); export default aclEntrySchema; diff --git a/packages/data-schemas/src/schema/action.ts b/packages/data-schemas/src/schema/action.ts index 4d5f64a0e1..5cde2ad6fc 100644 --- a/packages/data-schemas/src/schema/action.ts +++ b/packages/data-schemas/src/schema/action.ts @@ -47,6 +47,10 @@ const Action = new Schema({ oauth_client_id: String, oauth_client_secret: String, }, + tenantId: { + type: String, + index: true, + }, }); export default Action; diff --git a/packages/data-schemas/src/schema/agent.ts b/packages/data-schemas/src/schema/agent.ts index eff4b8e675..42a7ca5418 100644 --- a/packages/data-schemas/src/schema/agent.ts +++ b/packages/data-schemas/src/schema/agent.ts @@ -114,6 +114,10 @@ const agentSchema = new Schema( type: Schema.Types.Mixed, default: undefined, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, diff --git a/packages/data-schemas/src/schema/agentApiKey.ts b/packages/data-schemas/src/schema/agentApiKey.ts index d7037f857f..50334f5f5c 100644 --- a/packages/data-schemas/src/schema/agentApiKey.ts +++ b/packages/data-schemas/src/schema/agentApiKey.ts @@ -9,6 +9,7 @@ export interface IAgentApiKey extends Document { expiresAt?: Date; createdAt: Date; updatedAt: Date; + tenantId?: string; } const agentApiKeySchema: Schema = new Schema( @@ -42,11 +43,15 @@ const agentApiKeySchema: Schema = new Schema( expiresAt: { type: Date, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); -agentApiKeySchema.index({ userId: 1, name: 1 }); +agentApiKeySchema.index({ userId: 1, name: 1, tenantId: 1 }); /** * TTL index for automatic cleanup of expired keys. diff --git a/packages/data-schemas/src/schema/agentCategory.ts b/packages/data-schemas/src/schema/agentCategory.ts index d0d42f46c9..2922042129 100644 --- a/packages/data-schemas/src/schema/agentCategory.ts +++ b/packages/data-schemas/src/schema/agentCategory.ts @@ -6,7 +6,6 @@ const agentCategorySchema = new Schema( value: { type: String, required: true, - unique: true, trim: true, lowercase: true, index: true, @@ -35,12 +34,17 @@ const agentCategorySchema = new Schema( type: Boolean, default: false, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, }, ); +agentCategorySchema.index({ value: 1, tenantId: 1 }, { unique: true }); agentCategorySchema.index({ isActive: 1, order: 1 }); agentCategorySchema.index({ order: 1, label: 1 }); diff --git a/packages/data-schemas/src/schema/assistant.ts b/packages/data-schemas/src/schema/assistant.ts index 4f0226d38a..3fc052d458 100644 --- a/packages/data-schemas/src/schema/assistant.ts +++ b/packages/data-schemas/src/schema/assistant.ts @@ -30,6 +30,10 @@ const assistantSchema = new Schema( type: Boolean, default: false, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, diff --git a/packages/data-schemas/src/schema/balance.ts b/packages/data-schemas/src/schema/balance.ts index 8e786ae388..b9a65b8f4f 100644 --- a/packages/data-schemas/src/schema/balance.ts +++ b/packages/data-schemas/src/schema/balance.ts @@ -36,6 +36,10 @@ const balanceSchema = new Schema({ type: Number, default: 0, }, + tenantId: { + type: String, + index: true, + }, }); export default balanceSchema; diff --git a/packages/data-schemas/src/schema/banner.ts b/packages/data-schemas/src/schema/banner.ts index 7cd07a93af..73baa92b2f 100644 --- a/packages/data-schemas/src/schema/banner.ts +++ b/packages/data-schemas/src/schema/banner.ts @@ -8,6 +8,7 @@ export interface IBanner extends Document { type: 'banner' | 'popup'; isPublic: boolean; persistable: boolean; + tenantId?: string; } const bannerSchema = new Schema( @@ -41,6 +42,10 @@ const bannerSchema = new Schema( type: Boolean, default: false, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); diff --git a/packages/data-schemas/src/schema/categories.ts b/packages/data-schemas/src/schema/categories.ts index 5ebd2afb83..94832b54de 100644 --- a/packages/data-schemas/src/schema/categories.ts +++ b/packages/data-schemas/src/schema/categories.ts @@ -3,19 +3,25 @@ import { Schema, Document } from 'mongoose'; export interface ICategory extends Document { label: string; value: string; + tenantId?: string; } const categoriesSchema = new Schema({ label: { type: String, required: true, - unique: true, }, value: { type: String, required: true, - unique: true, + }, + tenantId: { + type: String, + index: true, }, }); +categoriesSchema.index({ label: 1, tenantId: 1 }, { unique: true }); +categoriesSchema.index({ value: 1, tenantId: 1 }, { unique: true }); + export default categoriesSchema; diff --git a/packages/data-schemas/src/schema/conversationTag.ts b/packages/data-schemas/src/schema/conversationTag.ts index e22231fdc2..6b37257121 100644 --- a/packages/data-schemas/src/schema/conversationTag.ts +++ b/packages/data-schemas/src/schema/conversationTag.ts @@ -6,6 +6,7 @@ export interface IConversationTag extends Document { description?: string; count?: number; position?: number; + tenantId?: string; } const conversationTag = new Schema( @@ -31,11 +32,15 @@ const conversationTag = new Schema( default: 0, index: true, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); // Create a compound index on tag and user with unique constraint. -conversationTag.index({ tag: 1, user: 1 }, { unique: true }); +conversationTag.index({ tag: 1, user: 1, tenantId: 1 }, { unique: true }); export default conversationTag; diff --git a/packages/data-schemas/src/schema/convo.ts b/packages/data-schemas/src/schema/convo.ts index e6a9ede6be..9ed8949e9c 100644 --- a/packages/data-schemas/src/schema/convo.ts +++ b/packages/data-schemas/src/schema/convo.ts @@ -37,13 +37,17 @@ const convoSchema: Schema = new Schema( expiredAt: { type: Date, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); convoSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 }); convoSchema.index({ createdAt: 1, updatedAt: 1 }); -convoSchema.index({ conversationId: 1, user: 1 }, { unique: true }); +convoSchema.index({ conversationId: 1, user: 1, tenantId: 1 }, { unique: true }); // index for MeiliSearch sync operations convoSchema.index({ _meiliIndex: 1, expiredAt: 1 }); diff --git a/packages/data-schemas/src/schema/file.ts b/packages/data-schemas/src/schema/file.ts index c5e3b3c4e3..e483541bdb 100644 --- a/packages/data-schemas/src/schema/file.ts +++ b/packages/data-schemas/src/schema/file.ts @@ -78,6 +78,10 @@ const file: Schema = new Schema( type: Date, expires: 3600, // 1 hour in seconds }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, @@ -86,7 +90,7 @@ const file: Schema = new Schema( file.index({ createdAt: 1, updatedAt: 1 }); file.index( - { filename: 1, conversationId: 1, context: 1 }, + { filename: 1, conversationId: 1, context: 1, tenantId: 1 }, { unique: true, partialFilterExpression: { context: FileContext.execute_code } }, ); diff --git a/packages/data-schemas/src/schema/group.ts b/packages/data-schemas/src/schema/group.ts index 55cb54e8b5..3cdbd31330 100644 --- a/packages/data-schemas/src/schema/group.ts +++ b/packages/data-schemas/src/schema/group.ts @@ -41,12 +41,16 @@ const groupSchema = new Schema( return this.source !== 'local'; }, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); groupSchema.index( - { idOnTheSource: 1, source: 1 }, + { idOnTheSource: 1, source: 1, tenantId: 1 }, { unique: true, partialFilterExpression: { idOnTheSource: { $exists: true } }, diff --git a/packages/data-schemas/src/schema/key.ts b/packages/data-schemas/src/schema/key.ts index 54857db753..330eb23471 100644 --- a/packages/data-schemas/src/schema/key.ts +++ b/packages/data-schemas/src/schema/key.ts @@ -5,6 +5,7 @@ export interface IKey extends Document { name: string; value: string; expiresAt?: Date; + tenantId?: string; } const keySchema: Schema = new Schema({ @@ -24,6 +25,10 @@ const keySchema: Schema = new Schema({ expiresAt: { type: Date, }, + tenantId: { + type: String, + index: true, + }, }); keySchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); diff --git a/packages/data-schemas/src/schema/mcpServer.ts b/packages/data-schemas/src/schema/mcpServer.ts index 8210c258d6..ac881932da 100644 --- a/packages/data-schemas/src/schema/mcpServer.ts +++ b/packages/data-schemas/src/schema/mcpServer.ts @@ -6,7 +6,6 @@ const mcpServerSchema = new Schema( serverName: { type: String, index: true, - unique: true, required: true, }, config: { @@ -20,12 +19,17 @@ const mcpServerSchema = new Schema( required: true, index: true, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, }, ); +mcpServerSchema.index({ serverName: 1, tenantId: 1 }, { unique: true }); mcpServerSchema.index({ updatedAt: -1, _id: 1 }); export default mcpServerSchema; diff --git a/packages/data-schemas/src/schema/memory.ts b/packages/data-schemas/src/schema/memory.ts index b6eadf04a7..773fa87115 100644 --- a/packages/data-schemas/src/schema/memory.ts +++ b/packages/data-schemas/src/schema/memory.ts @@ -28,6 +28,10 @@ const MemoryEntrySchema: Schema = new Schema({ type: Date, default: Date.now, }, + tenantId: { + type: String, + index: true, + }, }); export default MemoryEntrySchema; diff --git a/packages/data-schemas/src/schema/message.ts b/packages/data-schemas/src/schema/message.ts index f960194541..610251443d 100644 --- a/packages/data-schemas/src/schema/message.ts +++ b/packages/data-schemas/src/schema/message.ts @@ -144,13 +144,17 @@ const messageSchema: Schema = new Schema( type: Boolean, default: undefined, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); messageSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 }); messageSchema.index({ createdAt: 1 }); -messageSchema.index({ messageId: 1, user: 1 }, { unique: true }); +messageSchema.index({ messageId: 1, user: 1, tenantId: 1 }, { unique: true }); // index for MeiliSearch sync operations messageSchema.index({ _meiliIndex: 1, expiredAt: 1 }); diff --git a/packages/data-schemas/src/schema/pluginAuth.ts b/packages/data-schemas/src/schema/pluginAuth.ts index 534c49d127..e278e63d45 100644 --- a/packages/data-schemas/src/schema/pluginAuth.ts +++ b/packages/data-schemas/src/schema/pluginAuth.ts @@ -18,6 +18,10 @@ const pluginAuthSchema: Schema = new Schema( pluginKey: { type: String, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); diff --git a/packages/data-schemas/src/schema/preset.ts b/packages/data-schemas/src/schema/preset.ts index fc23d86c0b..33c217ea23 100644 --- a/packages/data-schemas/src/schema/preset.ts +++ b/packages/data-schemas/src/schema/preset.ts @@ -53,6 +53,7 @@ export interface IPreset extends Document { web_search?: boolean; disableStreaming?: boolean; fileTokenLimit?: number; + tenantId?: string; } const presetSchema: Schema = new Schema( @@ -79,6 +80,10 @@ const presetSchema: Schema = new Schema( type: Number, }, ...conversationPreset, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); diff --git a/packages/data-schemas/src/schema/prompt.ts b/packages/data-schemas/src/schema/prompt.ts index 017eeea5e3..dd32789727 100644 --- a/packages/data-schemas/src/schema/prompt.ts +++ b/packages/data-schemas/src/schema/prompt.ts @@ -23,6 +23,10 @@ const promptSchema: Schema = new Schema( enum: ['text', 'chat'], required: true, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, diff --git a/packages/data-schemas/src/schema/promptGroup.ts b/packages/data-schemas/src/schema/promptGroup.ts index d751c67557..bd4db546e3 100644 --- a/packages/data-schemas/src/schema/promptGroup.ts +++ b/packages/data-schemas/src/schema/promptGroup.ts @@ -53,6 +53,10 @@ const promptGroupSchema = new Schema( `Command cannot be longer than ${Constants.COMMANDS_MAX_LENGTH} characters`, ], }, // Casting here bypasses the type error for the command field. + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index e4821ba405..1c27478ef6 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -72,10 +72,16 @@ const rolePermissionsSchema = new Schema( ); const roleSchema: Schema = new Schema({ - name: { type: String, required: true, unique: true, index: true }, + name: { type: String, required: true, index: true }, permissions: { type: rolePermissionsSchema, }, + tenantId: { + type: String, + index: true, + }, }); +roleSchema.index({ name: 1, tenantId: 1 }, { unique: true }); + export default roleSchema; diff --git a/packages/data-schemas/src/schema/session.ts b/packages/data-schemas/src/schema/session.ts index 9dc2d733a5..4a66a37535 100644 --- a/packages/data-schemas/src/schema/session.ts +++ b/packages/data-schemas/src/schema/session.ts @@ -16,6 +16,10 @@ const sessionSchema: Schema = new Schema({ ref: 'User', required: true, }, + tenantId: { + type: String, + index: true, + }, }); export default sessionSchema; diff --git a/packages/data-schemas/src/schema/share.ts b/packages/data-schemas/src/schema/share.ts index 987dd10fc2..3238084889 100644 --- a/packages/data-schemas/src/schema/share.ts +++ b/packages/data-schemas/src/schema/share.ts @@ -10,6 +10,7 @@ export interface ISharedLink extends Document { isPublic: boolean; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } const shareSchema: Schema = new Schema( @@ -40,10 +41,14 @@ const shareSchema: Schema = new Schema( type: Boolean, default: true, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); -shareSchema.index({ conversationId: 1, user: 1, targetMessageId: 1 }); +shareSchema.index({ conversationId: 1, user: 1, targetMessageId: 1, tenantId: 1 }); export default shareSchema; diff --git a/packages/data-schemas/src/schema/token.ts b/packages/data-schemas/src/schema/token.ts index 8cb17eec5d..dae2118e64 100644 --- a/packages/data-schemas/src/schema/token.ts +++ b/packages/data-schemas/src/schema/token.ts @@ -33,6 +33,10 @@ const tokenSchema: Schema = new Schema({ type: Map, of: Schema.Types.Mixed, }, + tenantId: { + type: String, + index: true, + }, }); tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); diff --git a/packages/data-schemas/src/schema/toolCall.ts b/packages/data-schemas/src/schema/toolCall.ts index 4bc35e5799..d36d6b758a 100644 --- a/packages/data-schemas/src/schema/toolCall.ts +++ b/packages/data-schemas/src/schema/toolCall.ts @@ -12,6 +12,7 @@ export interface IToolCallData extends Document { partIndex?: number; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } const toolCallSchema: Schema = new Schema( @@ -45,11 +46,15 @@ const toolCallSchema: Schema = new Schema( partIndex: { type: Number, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); -toolCallSchema.index({ messageId: 1, user: 1 }); -toolCallSchema.index({ conversationId: 1, user: 1 }); +toolCallSchema.index({ messageId: 1, user: 1, tenantId: 1 }); +toolCallSchema.index({ conversationId: 1, user: 1, tenantId: 1 }); export default toolCallSchema; diff --git a/packages/data-schemas/src/schema/transaction.ts b/packages/data-schemas/src/schema/transaction.ts index 6faf684b12..bb41494696 100644 --- a/packages/data-schemas/src/schema/transaction.ts +++ b/packages/data-schemas/src/schema/transaction.ts @@ -17,6 +17,7 @@ export interface ITransaction extends Document { messageId?: string; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } const transactionSchema: Schema = new Schema( @@ -54,6 +55,10 @@ const transactionSchema: Schema = new Schema( writeTokens: { type: Number }, readTokens: { type: Number }, messageId: { type: String }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true, diff --git a/packages/data-schemas/src/schema/user.ts b/packages/data-schemas/src/schema/user.ts index 57c8f8574e..92680415bd 100644 --- a/packages/data-schemas/src/schema/user.ts +++ b/packages/data-schemas/src/schema/user.ts @@ -37,7 +37,6 @@ const userSchema = new Schema( type: String, required: [true, "can't be blank"], lowercase: true, - unique: true, match: [/\S+@\S+\.\S+/, 'is invalid'], index: true, }, @@ -68,43 +67,27 @@ const userSchema = new Schema( }, googleId: { type: String, - unique: true, - sparse: true, }, facebookId: { type: String, - unique: true, - sparse: true, }, openidId: { type: String, - unique: true, - sparse: true, }, samlId: { type: String, - unique: true, - sparse: true, }, ldapId: { type: String, - unique: true, - sparse: true, }, githubId: { type: String, - unique: true, - sparse: true, }, discordId: { type: String, - unique: true, - sparse: true, }, appleId: { type: String, - unique: true, - sparse: true, }, plugins: { type: Array, @@ -166,8 +149,32 @@ const userSchema = new Schema( type: String, sparse: true, }, + tenantId: { + type: String, + index: true, + }, }, { timestamps: true }, ); +userSchema.index({ email: 1, tenantId: 1 }, { unique: true }); + +const oAuthIdFields = [ + 'googleId', + 'facebookId', + 'openidId', + 'samlId', + 'ldapId', + 'githubId', + 'discordId', + 'appleId', +] as const; + +for (const field of oAuthIdFields) { + userSchema.index( + { [field]: 1, tenantId: 1 }, + { unique: true, partialFilterExpression: { [field]: { $exists: true } } }, + ); +} + export default userSchema; diff --git a/packages/data-schemas/src/types/accessRole.ts b/packages/data-schemas/src/types/accessRole.ts index 54f6aeb077..e873f9bdce 100644 --- a/packages/data-schemas/src/types/accessRole.ts +++ b/packages/data-schemas/src/types/accessRole.ts @@ -10,6 +10,7 @@ export type AccessRole = { resourceType: string; /** e.g., 1 for read, 3 for read+write */ permBits: number; + tenantId?: string; }; export type IAccessRole = AccessRole & diff --git a/packages/data-schemas/src/types/aclEntry.ts b/packages/data-schemas/src/types/aclEntry.ts index 026b852aa8..ae5860a599 100644 --- a/packages/data-schemas/src/types/aclEntry.ts +++ b/packages/data-schemas/src/types/aclEntry.ts @@ -22,6 +22,7 @@ export type AclEntry = { grantedBy?: Types.ObjectId; /** When this permission was granted */ grantedAt?: Date; + tenantId?: string; }; export type IAclEntry = AclEntry & diff --git a/packages/data-schemas/src/types/action.ts b/packages/data-schemas/src/types/action.ts index 6a269856dd..841d6c95e5 100644 --- a/packages/data-schemas/src/types/action.ts +++ b/packages/data-schemas/src/types/action.ts @@ -25,4 +25,5 @@ export interface IAction extends Document { oauth_client_id?: string; oauth_client_secret?: string; }; + tenantId?: string; } diff --git a/packages/data-schemas/src/types/agent.ts b/packages/data-schemas/src/types/agent.ts index 1171028c5d..2af6c22439 100644 --- a/packages/data-schemas/src/types/agent.ts +++ b/packages/data-schemas/src/types/agent.ts @@ -41,4 +41,5 @@ export interface IAgent extends Omit { mcpServerNames?: string[]; /** Per-tool configuration (defer_loading, allowed_callers) */ tool_options?: AgentToolOptions; + tenantId?: string; } diff --git a/packages/data-schemas/src/types/agentApiKey.ts b/packages/data-schemas/src/types/agentApiKey.ts index 968937a717..c5e7836edd 100644 --- a/packages/data-schemas/src/types/agentApiKey.ts +++ b/packages/data-schemas/src/types/agentApiKey.ts @@ -9,6 +9,7 @@ export interface IAgentApiKey extends Document { expiresAt?: Date; createdAt: Date; updatedAt: Date; + tenantId?: string; } export interface AgentApiKeyCreateData { diff --git a/packages/data-schemas/src/types/agentCategory.ts b/packages/data-schemas/src/types/agentCategory.ts index 1a814d289f..1c1648bac1 100644 --- a/packages/data-schemas/src/types/agentCategory.ts +++ b/packages/data-schemas/src/types/agentCategory.ts @@ -13,6 +13,7 @@ export type AgentCategory = { isActive: boolean; /** Whether this is a custom user-created category */ custom?: boolean; + tenantId?: string; }; export type IAgentCategory = AgentCategory & diff --git a/packages/data-schemas/src/types/assistant.ts b/packages/data-schemas/src/types/assistant.ts index d2e180c668..33381f7399 100644 --- a/packages/data-schemas/src/types/assistant.ts +++ b/packages/data-schemas/src/types/assistant.ts @@ -12,4 +12,5 @@ export interface IAssistant extends Document { file_ids?: string[]; actions?: string[]; append_current_datetime?: boolean; + tenantId?: string; } diff --git a/packages/data-schemas/src/types/balance.ts b/packages/data-schemas/src/types/balance.ts index e5eb4c4f15..54ceb0a1e9 100644 --- a/packages/data-schemas/src/types/balance.ts +++ b/packages/data-schemas/src/types/balance.ts @@ -9,6 +9,7 @@ export interface IBalance extends Document { refillIntervalUnit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months'; lastRefill: Date; refillAmount: number; + tenantId?: string; } /** Plain data fields for creating or updating a balance record (no Mongoose Document methods) */ diff --git a/packages/data-schemas/src/types/banner.ts b/packages/data-schemas/src/types/banner.ts index e9c63ac97e..756718e5d1 100644 --- a/packages/data-schemas/src/types/banner.ts +++ b/packages/data-schemas/src/types/banner.ts @@ -8,4 +8,5 @@ export interface IBanner extends Document { type: 'banner' | 'popup'; isPublic: boolean; persistable: boolean; + tenantId?: string; } diff --git a/packages/data-schemas/src/types/convo.ts b/packages/data-schemas/src/types/convo.ts index 43965a5827..c7888efba2 100644 --- a/packages/data-schemas/src/types/convo.ts +++ b/packages/data-schemas/src/types/convo.ts @@ -56,4 +56,5 @@ export interface IConversation extends Document { expiredAt?: Date; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } diff --git a/packages/data-schemas/src/types/file.ts b/packages/data-schemas/src/types/file.ts index 8f17e3b597..bbf9de3d3d 100644 --- a/packages/data-schemas/src/types/file.ts +++ b/packages/data-schemas/src/types/file.ts @@ -25,4 +25,5 @@ export interface IMongoFile extends Omit { expiresAt?: Date; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } diff --git a/packages/data-schemas/src/types/group.ts b/packages/data-schemas/src/types/group.ts index e0e622aca2..15cb6f288e 100644 --- a/packages/data-schemas/src/types/group.ts +++ b/packages/data-schemas/src/types/group.ts @@ -14,6 +14,7 @@ export interface IGroup extends Document { idOnTheSource?: string; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } export interface CreateGroupRequest { diff --git a/packages/data-schemas/src/types/mcp.ts b/packages/data-schemas/src/types/mcp.ts index 9b1c622293..560d535737 100644 --- a/packages/data-schemas/src/types/mcp.ts +++ b/packages/data-schemas/src/types/mcp.ts @@ -9,4 +9,5 @@ export interface MCPServerDocument extends Omit, Document { author: Types.ObjectId; // ObjectId reference in DB (vs string in API) + tenantId?: string; } diff --git a/packages/data-schemas/src/types/memory.ts b/packages/data-schemas/src/types/memory.ts index 6ab6c29345..4d9b4fbefd 100644 --- a/packages/data-schemas/src/types/memory.ts +++ b/packages/data-schemas/src/types/memory.ts @@ -7,6 +7,7 @@ export interface IMemoryEntry extends Document { value: string; tokenCount?: number; updated_at?: Date; + tenantId?: string; } export interface IMemoryEntryLean { diff --git a/packages/data-schemas/src/types/message.ts b/packages/data-schemas/src/types/message.ts index c4e96b34ba..c3f465e711 100644 --- a/packages/data-schemas/src/types/message.ts +++ b/packages/data-schemas/src/types/message.ts @@ -43,4 +43,5 @@ export interface IMessage extends Document { expiredAt?: Date | null; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } diff --git a/packages/data-schemas/src/types/pluginAuth.ts b/packages/data-schemas/src/types/pluginAuth.ts index c38bc790ab..fb5c5fc4e7 100644 --- a/packages/data-schemas/src/types/pluginAuth.ts +++ b/packages/data-schemas/src/types/pluginAuth.ts @@ -7,6 +7,7 @@ export interface IPluginAuth extends Document { pluginKey?: string; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } export interface PluginAuthQuery { diff --git a/packages/data-schemas/src/types/prompts.ts b/packages/data-schemas/src/types/prompts.ts index 53f09dcd49..02db35a1be 100644 --- a/packages/data-schemas/src/types/prompts.ts +++ b/packages/data-schemas/src/types/prompts.ts @@ -7,6 +7,7 @@ export interface IPrompt extends Document { type: 'text' | 'chat'; createdAt?: Date; updatedAt?: Date; + tenantId?: string; } export interface IPromptGroup { @@ -21,6 +22,7 @@ export interface IPromptGroup { createdAt?: Date; updatedAt?: Date; isPublic?: boolean; + tenantId?: string; } export interface IPromptGroupDocument extends IPromptGroup, Document {} diff --git a/packages/data-schemas/src/types/role.ts b/packages/data-schemas/src/types/role.ts index e70e29204a..60a579240c 100644 --- a/packages/data-schemas/src/types/role.ts +++ b/packages/data-schemas/src/types/role.ts @@ -66,6 +66,7 @@ export interface IRole extends Document { [Permissions.SHARE_PUBLIC]?: boolean; }; }; + tenantId?: string; } export type RolePermissions = IRole['permissions']; diff --git a/packages/data-schemas/src/types/session.ts b/packages/data-schemas/src/types/session.ts index a7e9591e12..db01a23162 100644 --- a/packages/data-schemas/src/types/session.ts +++ b/packages/data-schemas/src/types/session.ts @@ -4,6 +4,7 @@ export interface ISession extends Document { refreshTokenHash: string; expiration: Date; user: Types.ObjectId; + tenantId?: string; } export interface CreateSessionOptions { diff --git a/packages/data-schemas/src/types/token.ts b/packages/data-schemas/src/types/token.ts index e71958a1d9..063e18a7c9 100644 --- a/packages/data-schemas/src/types/token.ts +++ b/packages/data-schemas/src/types/token.ts @@ -9,6 +9,7 @@ export interface IToken extends Document { createdAt: Date; expiresAt: Date; metadata?: Map; + tenantId?: string; } export interface TokenCreateData { diff --git a/packages/data-schemas/src/types/user.ts b/packages/data-schemas/src/types/user.ts index e1cecb7518..0fac46ee63 100644 --- a/packages/data-schemas/src/types/user.ts +++ b/packages/data-schemas/src/types/user.ts @@ -49,6 +49,7 @@ export interface IUser extends Document { updatedAt?: Date; /** Field for external source identification (for consistency with TPrincipal schema) */ idOnTheSource?: string; + tenantId?: string; } export interface BalanceConfig { From a0fed6173c189aefac2dfd19f9d7f8542ea5edf0 Mon Sep 17 00:00:00 2001 From: Atef Bellaaj Date: Mon, 9 Mar 2026 20:42:01 +0100 Subject: [PATCH 099/111] =?UTF-8?q?=F0=9F=97=82=EF=B8=8F=20refactor:=20Mig?= =?UTF-8?q?rate=20S3=20Storage=20to=20TypeScript=20in=20packages/api=20(#1?= =?UTF-8?q?1947)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate S3 storage module with unit and integration tests - Migrate S3 CRUD and image operations to packages/api/src/storage/s3/ - Add S3ImageService class with dependency injection - Add unit tests using aws-sdk-client-mock - Add integration tests with real s3 bucket (condition presence of AWS_TEST_BUCKET_NAME) * AI Review Findings Fixes * chore: tests and refactor S3 storage types - Added mock implementations for the 'sharp' library in various test files to improve image processing testing. - Updated type references in S3 storage files from MongoFile to TFile for consistency and type safety. - Refactored S3 CRUD operations to ensure proper handling of file types and improve code clarity. - Enhanced integration tests to validate S3 file operations and error handling more effectively. * chore: rename test file * Remove duplicate import of refreshS3Url * chore: imports order * fix: remove duplicate imports for S3 URL handling in UserController * fix: remove duplicate import of refreshS3FileUrls in files.js * test: Add mock implementations for 'sharp' and '@librechat/api' in UserController tests - Introduced mock functions for the 'sharp' library to facilitate image processing tests, including metadata retrieval and buffer conversion. - Enhanced mocking for '@librechat/api' to ensure consistent behavior in tests, particularly for the needsRefresh and getNewS3URL functions. --------- Co-authored-by: Danny Avila --- api/server/controllers/UserController.js | 3 +- api/server/controllers/UserController.spec.js | 11 +- api/server/controllers/agents/v1.js | 2 +- api/server/controllers/agents/v1.spec.js | 13 +- api/server/routes/files/files.agents.test.js | 11 +- api/server/routes/files/files.js | 10 +- api/server/routes/files/files.test.js | 11 +- api/server/services/Files/S3/crud.js | 556 ----------- api/server/services/Files/S3/images.js | 129 --- api/server/services/Files/S3/index.js | 7 - api/server/services/Files/strategies.js | 30 +- .../server/services/Files/S3/crud.test.js | 72 -- api/test/services/Files/S3/crud.test.js | 876 ------------------ package-lock.json | 206 +++- packages/api/package.json | 3 + packages/api/src/cdn/__tests__/s3.test.ts | 8 + packages/api/src/cdn/s3.ts | 7 + packages/api/src/index.ts | 2 + packages/api/src/storage/index.ts | 2 + .../api/src/storage/s3/__tests__/crud.test.ts | 770 +++++++++++++++ .../src/storage/s3/__tests__/images.test.ts | 182 ++++ .../s3/__tests__/s3.integration.spec.ts | 529 +++++++++++ packages/api/src/storage/s3/crud.ts | 460 +++++++++ packages/api/src/storage/s3/images.ts | 141 +++ packages/api/src/storage/s3/index.ts | 2 + packages/api/src/storage/s3/s3Config.ts | 57 ++ packages/api/src/storage/types.ts | 60 ++ 27 files changed, 2460 insertions(+), 1700 deletions(-) delete mode 100644 api/server/services/Files/S3/crud.js delete mode 100644 api/server/services/Files/S3/images.js delete mode 100644 api/server/services/Files/S3/index.js delete mode 100644 api/test/server/services/Files/S3/crud.test.js delete mode 100644 api/test/services/Files/S3/crud.test.js create mode 100644 packages/api/src/storage/index.ts create mode 100644 packages/api/src/storage/s3/__tests__/crud.test.ts create mode 100644 packages/api/src/storage/s3/__tests__/images.test.ts create mode 100644 packages/api/src/storage/s3/__tests__/s3.integration.spec.ts create mode 100644 packages/api/src/storage/s3/crud.ts create mode 100644 packages/api/src/storage/s3/images.ts create mode 100644 packages/api/src/storage/s3/index.ts create mode 100644 packages/api/src/storage/s3/s3Config.ts create mode 100644 packages/api/src/storage/types.ts diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 4a1c9135ab..3702f190db 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -1,6 +1,8 @@ const mongoose = require('mongoose'); const { logger, webSearchKeys } = require('@librechat/data-schemas'); const { + getNewS3URL, + needsRefresh, MCPOAuthHandler, MCPTokenStorage, normalizeHttpError, @@ -18,7 +20,6 @@ const { verifyOTPOrBackupCode } = require('~/server/services/twoFactorService'); const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService'); const { getMCPManager, getFlowStateManager, getMCPServersRegistry } = require('~/config'); const { invalidateCachedTools } = require('~/server/services/Config/getCachedTools'); -const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud'); const { processDeleteRequest } = require('~/server/services/Files/process'); const { getAppConfig } = require('~/server/services/Config'); const { getSoleOwnedResourceIds } = require('~/server/services/PermissionService'); diff --git a/api/server/controllers/UserController.spec.js b/api/server/controllers/UserController.spec.js index 6c96f067b7..4a96072062 100644 --- a/api/server/controllers/UserController.spec.js +++ b/api/server/controllers/UserController.spec.js @@ -59,7 +59,16 @@ jest.mock('~/server/services/AuthService', () => ({ resendVerificationEmail: jest.fn(), })); -jest.mock('~/server/services/Files/S3/crud', () => ({ +jest.mock('sharp', () => + jest.fn(() => ({ + metadata: jest.fn().mockResolvedValue({}), + toFormat: jest.fn().mockReturnThis(), + toBuffer: jest.fn().mockResolvedValue(Buffer.alloc(0)), + })), +); + +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), needsRefresh: jest.fn(), getNewS3URL: jest.fn(), })); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 40d80d571f..b6eb4fc22c 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -3,6 +3,7 @@ const fs = require('fs').promises; const { nanoid } = require('nanoid'); const { logger } = require('@librechat/data-schemas'); const { + refreshS3Url, agentCreateSchema, agentUpdateSchema, refreshListAvatars, @@ -35,7 +36,6 @@ const { const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { resizeAvatar } = require('~/server/services/Files/images/avatar'); const { getFileStrategy } = require('~/server/utils/getFileStrategy'); -const { refreshS3Url } = require('~/server/services/Files/S3/crud'); const { filterFile } = require('~/server/services/Files/process'); const { getCachedTools } = require('~/server/services/Config'); const { getMCPServersRegistry } = require('~/config'); diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 9a8dd0a50a..455cea2e7c 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -22,7 +22,16 @@ jest.mock('~/server/services/Files/images/avatar', () => ({ resizeAvatar: jest.fn(), })); -jest.mock('~/server/services/Files/S3/crud', () => ({ +jest.mock('sharp', () => + jest.fn(() => ({ + metadata: jest.fn().mockResolvedValue({}), + toFormat: jest.fn().mockReturnThis(), + toBuffer: jest.fn().mockResolvedValue(Buffer.alloc(0)), + })), +); + +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), refreshS3Url: jest.fn(), })); @@ -73,7 +82,7 @@ const { getResourcePermissionsMap, } = require('~/server/services/PermissionService'); -const { refreshS3Url } = require('~/server/services/Files/S3/crud'); +const { refreshS3Url } = require('@librechat/api'); /** * @type {import('mongoose').Model} diff --git a/api/server/routes/files/files.agents.test.js b/api/server/routes/files/files.agents.test.js index e64be9cf4e..5a01df022d 100644 --- a/api/server/routes/files/files.agents.test.js +++ b/api/server/routes/files/files.agents.test.js @@ -39,7 +39,16 @@ jest.mock('~/server/services/Tools/credentials', () => ({ loadAuthValues: jest.fn(), })); -jest.mock('~/server/services/Files/S3/crud', () => ({ +jest.mock('sharp', () => + jest.fn(() => ({ + metadata: jest.fn().mockResolvedValue({}), + toFormat: jest.fn().mockReturnThis(), + toBuffer: jest.fn().mockResolvedValue(Buffer.alloc(0)), + })), +); + +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), refreshS3FileUrls: jest.fn(), })); diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index 5578fc6474..e1b420fb5d 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -1,8 +1,12 @@ const fs = require('fs').promises; const express = require('express'); const { EnvVar } = require('@librechat/agents'); -const { logger } = require('@librechat/data-schemas'); -const { verifyAgentUploadPermission, resolveUploadErrorMessage } = require('@librechat/api'); +const { logger, SystemCapabilities } = require('@librechat/data-schemas'); +const { + refreshS3FileUrls, + resolveUploadErrorMessage, + verifyAgentUploadPermission, +} = require('@librechat/api'); const { Time, isUUID, @@ -14,7 +18,6 @@ const { checkOpenAIStorage, isAssistantsEndpoint, } = require('librechat-data-provider'); -const { SystemCapabilities } = require('@librechat/data-schemas'); const { filterFile, processFileUpload, @@ -26,7 +29,6 @@ const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { checkPermission } = require('~/server/services/PermissionService'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); -const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud'); const { hasAccessToFilesViaAgent } = require('~/server/services/Files'); const { cleanFileName } = require('~/server/utils/files'); const { hasCapability } = require('~/server/middleware'); diff --git a/api/server/routes/files/files.test.js b/api/server/routes/files/files.test.js index 457ebabe92..37cbf68b93 100644 --- a/api/server/routes/files/files.test.js +++ b/api/server/routes/files/files.test.js @@ -32,7 +32,16 @@ jest.mock('~/server/services/Tools/credentials', () => ({ loadAuthValues: jest.fn(), })); -jest.mock('~/server/services/Files/S3/crud', () => ({ +jest.mock('sharp', () => + jest.fn(() => ({ + metadata: jest.fn().mockResolvedValue({}), + toFormat: jest.fn().mockReturnThis(), + toBuffer: jest.fn().mockResolvedValue(Buffer.alloc(0)), + })), +); + +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), refreshS3FileUrls: jest.fn(), })); diff --git a/api/server/services/Files/S3/crud.js b/api/server/services/Files/S3/crud.js deleted file mode 100644 index c821c0696c..0000000000 --- a/api/server/services/Files/S3/crud.js +++ /dev/null @@ -1,556 +0,0 @@ -const fs = require('fs'); -const fetch = require('node-fetch'); -const { logger } = require('@librechat/data-schemas'); -const { FileSources } = require('librechat-data-provider'); -const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); -const { initializeS3, deleteRagFile, isEnabled } = require('@librechat/api'); -const { - PutObjectCommand, - GetObjectCommand, - HeadObjectCommand, - DeleteObjectCommand, -} = require('@aws-sdk/client-s3'); - -const bucketName = process.env.AWS_BUCKET_NAME; -const defaultBasePath = 'images'; -const endpoint = process.env.AWS_ENDPOINT_URL; -const forcePathStyle = isEnabled(process.env.AWS_FORCE_PATH_STYLE); - -let s3UrlExpirySeconds = 2 * 60; // 2 minutes -let s3RefreshExpiryMs = null; - -if (process.env.S3_URL_EXPIRY_SECONDS !== undefined) { - const parsed = parseInt(process.env.S3_URL_EXPIRY_SECONDS, 10); - - if (!isNaN(parsed) && parsed > 0) { - s3UrlExpirySeconds = Math.min(parsed, 7 * 24 * 60 * 60); - } else { - logger.warn( - `[S3] Invalid S3_URL_EXPIRY_SECONDS value: "${process.env.S3_URL_EXPIRY_SECONDS}". Using 2-minute expiry.`, - ); - } -} - -if (process.env.S3_REFRESH_EXPIRY_MS !== null && process.env.S3_REFRESH_EXPIRY_MS) { - const parsed = parseInt(process.env.S3_REFRESH_EXPIRY_MS, 10); - - if (!isNaN(parsed) && parsed > 0) { - s3RefreshExpiryMs = parsed; - logger.info(`[S3] Using custom refresh expiry time: ${s3RefreshExpiryMs}ms`); - } else { - logger.warn( - `[S3] Invalid S3_REFRESH_EXPIRY_MS value: "${process.env.S3_REFRESH_EXPIRY_MS}". Using default refresh logic.`, - ); - } -} - -/** - * Constructs the S3 key based on the base path, user ID, and file name. - */ -const getS3Key = (basePath, userId, fileName) => `${basePath}/${userId}/${fileName}`; - -/** - * Uploads a buffer to S3 and returns a signed URL. - * - * @param {Object} params - * @param {string} params.userId - The user's unique identifier. - * @param {Buffer} params.buffer - The buffer containing file data. - * @param {string} params.fileName - The file name to use in S3. - * @param {string} [params.basePath='images'] - The base path in the bucket. - * @returns {Promise} Signed URL of the uploaded file. - */ -async function saveBufferToS3({ userId, buffer, fileName, basePath = defaultBasePath }) { - const key = getS3Key(basePath, userId, fileName); - const params = { Bucket: bucketName, Key: key, Body: buffer }; - - try { - const s3 = initializeS3(); - await s3.send(new PutObjectCommand(params)); - return await getS3URL({ userId, fileName, basePath }); - } catch (error) { - logger.error('[saveBufferToS3] Error uploading buffer to S3:', error.message); - throw error; - } -} - -/** - * Retrieves a URL for a file stored in S3. - * Returns a signed URL with expiration time or a proxy URL based on config - * - * @param {Object} params - * @param {string} params.userId - The user's unique identifier. - * @param {string} params.fileName - The file name in S3. - * @param {string} [params.basePath='images'] - The base path in the bucket. - * @param {string} [params.customFilename] - Custom filename for Content-Disposition header (overrides extracted filename). - * @param {string} [params.contentType] - Custom content type for the response. - * @returns {Promise} A URL to access the S3 object - */ -async function getS3URL({ - userId, - fileName, - basePath = defaultBasePath, - customFilename = null, - contentType = null, -}) { - const key = getS3Key(basePath, userId, fileName); - const params = { Bucket: bucketName, Key: key }; - - // Add response headers if specified - if (customFilename) { - params.ResponseContentDisposition = `attachment; filename="${customFilename}"`; - } - - if (contentType) { - params.ResponseContentType = contentType; - } - - try { - const s3 = initializeS3(); - return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: s3UrlExpirySeconds }); - } catch (error) { - logger.error('[getS3URL] Error getting signed URL from S3:', error.message); - throw error; - } -} - -/** - * Saves a file from a given URL to S3. - * - * @param {Object} params - * @param {string} params.userId - The user's unique identifier. - * @param {string} params.URL - The source URL of the file. - * @param {string} params.fileName - The file name to use in S3. - * @param {string} [params.basePath='images'] - The base path in the bucket. - * @returns {Promise} Signed URL of the uploaded file. - */ -async function saveURLToS3({ userId, URL, fileName, basePath = defaultBasePath }) { - try { - const response = await fetch(URL); - const buffer = await response.buffer(); - // Optionally you can call getBufferMetadata(buffer) if needed. - return await saveBufferToS3({ userId, buffer, fileName, basePath }); - } catch (error) { - logger.error('[saveURLToS3] Error uploading file from URL to S3:', error.message); - throw error; - } -} - -/** - * Deletes a file from S3. - * - * @param {Object} params - * @param {ServerRequest} params.req - * @param {MongoFile} params.file - The file object to delete. - * @returns {Promise} - */ -async function deleteFileFromS3(req, file) { - await deleteRagFile({ userId: req.user.id, file }); - - const key = extractKeyFromS3Url(file.filepath); - const params = { Bucket: bucketName, Key: key }; - if (!key.includes(req.user.id)) { - const message = `[deleteFileFromS3] User ID mismatch: ${req.user.id} vs ${key}`; - logger.error(message); - throw new Error(message); - } - - try { - const s3 = initializeS3(); - - try { - const headCommand = new HeadObjectCommand(params); - await s3.send(headCommand); - logger.debug('[deleteFileFromS3] File exists, proceeding with deletion'); - } catch (headErr) { - if (headErr.name === 'NotFound') { - logger.warn(`[deleteFileFromS3] File does not exist: ${key}`); - return; - } - } - - const deleteResult = await s3.send(new DeleteObjectCommand(params)); - logger.debug('[deleteFileFromS3] Delete command response:', JSON.stringify(deleteResult)); - try { - await s3.send(new HeadObjectCommand(params)); - logger.error('[deleteFileFromS3] File still exists after deletion!'); - } catch (verifyErr) { - if (verifyErr.name === 'NotFound') { - logger.debug(`[deleteFileFromS3] Verified file is deleted: ${key}`); - } else { - logger.error('[deleteFileFromS3] Error verifying deletion:', verifyErr); - } - } - - logger.debug('[deleteFileFromS3] S3 File deletion completed'); - } catch (error) { - logger.error(`[deleteFileFromS3] Error deleting file from S3: ${error.message}`); - logger.error(error.stack); - - // If the file is not found, we can safely return. - if (error.code === 'NoSuchKey') { - return; - } - throw error; - } -} - -/** - * Uploads a local file to S3 by streaming it directly without loading into memory. - * - * @param {Object} params - * @param {import('express').Request} params.req - The Express request (must include user). - * @param {Express.Multer.File} params.file - The file object from Multer. - * @param {string} params.file_id - Unique file identifier. - * @param {string} [params.basePath='images'] - The base path in the bucket. - * @returns {Promise<{ filepath: string, bytes: number }>} - */ -async function uploadFileToS3({ req, file, file_id, basePath = defaultBasePath }) { - try { - const inputFilePath = file.path; - const userId = req.user.id; - const fileName = `${file_id}__${file.originalname}`; - const key = getS3Key(basePath, userId, fileName); - - const stats = await fs.promises.stat(inputFilePath); - const bytes = stats.size; - const fileStream = fs.createReadStream(inputFilePath); - - const s3 = initializeS3(); - const uploadParams = { - Bucket: bucketName, - Key: key, - Body: fileStream, - }; - - await s3.send(new PutObjectCommand(uploadParams)); - const fileURL = await getS3URL({ userId, fileName, basePath }); - return { filepath: fileURL, bytes }; - } catch (error) { - logger.error('[uploadFileToS3] Error streaming file to S3:', error); - try { - if (file && file.path) { - await fs.promises.unlink(file.path); - } - } catch (unlinkError) { - logger.error( - '[uploadFileToS3] Error deleting temporary file, likely already deleted:', - unlinkError.message, - ); - } - throw error; - } -} - -/** - * Extracts the S3 key from a URL or returns the key if already properly formatted - * - * @param {string} fileUrlOrKey - The file URL or key - * @returns {string} The S3 key - */ -function extractKeyFromS3Url(fileUrlOrKey) { - if (!fileUrlOrKey) { - throw new Error('Invalid input: URL or key is empty'); - } - - try { - const url = new URL(fileUrlOrKey); - const hostname = url.hostname; - const pathname = url.pathname.substring(1); // Remove leading slash - - // Explicit path-style with custom endpoint: use endpoint pathname for precise key extraction. - // Handles endpoints with a base path (e.g. https://example.com/storage/). - if (endpoint && forcePathStyle) { - const endpointUrl = new URL(endpoint); - const startPos = - endpointUrl.pathname.length + - (endpointUrl.pathname.endsWith('/') ? 0 : 1) + - bucketName.length + - 1; - const key = url.pathname.substring(startPos); - if (!key) { - logger.warn( - `[extractKeyFromS3Url] Extracted key is empty for endpoint path-style URL: ${fileUrlOrKey}`, - ); - } else { - logger.debug(`[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`); - } - return key; - } - - if ( - hostname === 's3.amazonaws.com' || - hostname.match(/^s3[-.][a-z0-9-]+\.amazonaws\.com$/) || - (bucketName && pathname.startsWith(`${bucketName}/`)) - ) { - // Path-style: https://s3.amazonaws.com/bucket-name/key or custom endpoint (MinIO, R2, etc.) - // Strip the bucket name (first path segment) - const firstSlashIndex = pathname.indexOf('/'); - if (firstSlashIndex > 0) { - const key = pathname.substring(firstSlashIndex + 1); - - if (key === '') { - logger.warn( - `[extractKeyFromS3Url] Extracted key is empty after removing bucket name from URL: ${fileUrlOrKey}`, - ); - } else { - logger.debug( - `[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`, - ); - } - - return key; - } else { - logger.warn( - `[extractKeyFromS3Url] Unable to extract key from path-style URL: ${fileUrlOrKey}`, - ); - return ''; - } - } - - // Virtual-hosted-style or other: https://bucket-name.s3.amazonaws.com/key - // Just return the pathname without leading slash - logger.debug(`[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${pathname}`); - return pathname; - } catch (error) { - if (fileUrlOrKey.startsWith('http://') || fileUrlOrKey.startsWith('https://')) { - logger.error( - `[extractKeyFromS3Url] Error parsing URL: ${fileUrlOrKey}, Error: ${error.message}`, - ); - } else { - logger.debug(`[extractKeyFromS3Url] Non-URL input, using fallback: ${fileUrlOrKey}`); - } - - const parts = fileUrlOrKey.split('/'); - - if (parts.length >= 3 && !fileUrlOrKey.startsWith('http') && !fileUrlOrKey.startsWith('/')) { - return fileUrlOrKey; - } - - const key = fileUrlOrKey.startsWith('/') ? fileUrlOrKey.substring(1) : fileUrlOrKey; - logger.debug( - `[extractKeyFromS3Url] FALLBACK. fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`, - ); - return key; - } -} - -/** - * Retrieves a readable stream for a file stored in S3. - * - * @param {ServerRequest} req - Server request object. - * @param {string} filePath - The S3 key of the file. - * @returns {Promise} - */ -async function getS3FileStream(_req, filePath) { - try { - const Key = extractKeyFromS3Url(filePath); - const params = { Bucket: bucketName, Key }; - const s3 = initializeS3(); - const data = await s3.send(new GetObjectCommand(params)); - return data.Body; // Returns a Node.js ReadableStream. - } catch (error) { - logger.error('[getS3FileStream] Error retrieving S3 file stream:', error); - throw error; - } -} - -/** - * Determines if a signed S3 URL is close to expiration - * - * @param {string} signedUrl - The signed S3 URL - * @param {number} bufferSeconds - Buffer time in seconds - * @returns {boolean} True if the URL needs refreshing - */ -function needsRefresh(signedUrl, bufferSeconds) { - try { - // Parse the URL - const url = new URL(signedUrl); - - // Check if it has the signature parameters that indicate it's a signed URL - // X-Amz-Signature is the most reliable indicator for AWS signed URLs - if (!url.searchParams.has('X-Amz-Signature')) { - // Not a signed URL, so no expiration to check (or it's already a proxy URL) - return false; - } - - // Extract the expiration time from the URL - const expiresParam = url.searchParams.get('X-Amz-Expires'); - const dateParam = url.searchParams.get('X-Amz-Date'); - - if (!expiresParam || !dateParam) { - // Missing expiration information, assume it needs refresh to be safe - return true; - } - - // Parse the AWS date format (YYYYMMDDTHHMMSSZ) - const year = dateParam.substring(0, 4); - const month = dateParam.substring(4, 6); - const day = dateParam.substring(6, 8); - const hour = dateParam.substring(9, 11); - const minute = dateParam.substring(11, 13); - const second = dateParam.substring(13, 15); - - const dateObj = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`); - const expiresAtDate = new Date(dateObj.getTime() + parseInt(expiresParam) * 1000); - - // Check if it's close to expiration - const now = new Date(); - - // If S3_REFRESH_EXPIRY_MS is set, use it to determine if URL is expired - if (s3RefreshExpiryMs !== null) { - const urlCreationTime = dateObj.getTime(); - const urlAge = now.getTime() - urlCreationTime; - return urlAge >= s3RefreshExpiryMs; - } - - // Otherwise use the default buffer-based logic - const bufferTime = new Date(now.getTime() + bufferSeconds * 1000); - return expiresAtDate <= bufferTime; - } catch (error) { - logger.error('Error checking URL expiration:', error); - // If we can't determine, assume it needs refresh to be safe - return true; - } -} - -/** - * Generates a new URL for an expired S3 URL - * @param {string} currentURL - The current file URL - * @returns {Promise} - */ -async function getNewS3URL(currentURL) { - try { - const s3Key = extractKeyFromS3Url(currentURL); - if (!s3Key) { - return; - } - const keyParts = s3Key.split('/'); - if (keyParts.length < 3) { - return; - } - - const basePath = keyParts[0]; - const userId = keyParts[1]; - const fileName = keyParts.slice(2).join('/'); - - return await getS3URL({ - userId, - fileName, - basePath, - }); - } catch (error) { - logger.error('Error getting new S3 URL:', error); - } -} - -/** - * Refreshes S3 URLs for an array of files if they're expired or close to expiring - * - * @param {MongoFile[]} files - Array of file documents - * @param {(files: MongoFile[]) => Promise} batchUpdateFiles - Function to update files in the database - * @param {number} [bufferSeconds=3600] - Buffer time in seconds to check for expiration - * @returns {Promise} The files with refreshed URLs if needed - */ -async function refreshS3FileUrls(files, batchUpdateFiles, bufferSeconds = 3600) { - if (!files || !Array.isArray(files) || files.length === 0) { - return files; - } - - const filesToUpdate = []; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (!file?.file_id) { - continue; - } - if (file.source !== FileSources.s3) { - continue; - } - if (!file.filepath) { - continue; - } - if (!needsRefresh(file.filepath, bufferSeconds)) { - continue; - } - try { - const newURL = await getNewS3URL(file.filepath); - if (!newURL) { - continue; - } - filesToUpdate.push({ - file_id: file.file_id, - filepath: newURL, - }); - files[i].filepath = newURL; - } catch (error) { - logger.error(`Error refreshing S3 URL for file ${file.file_id}:`, error); - } - } - - if (filesToUpdate.length > 0) { - await batchUpdateFiles(filesToUpdate); - } - - return files; -} - -/** - * Refreshes a single S3 URL if it's expired or close to expiring - * - * @param {{ filepath: string, source: string }} fileObj - Simple file object containing filepath and source - * @param {number} [bufferSeconds=3600] - Buffer time in seconds to check for expiration - * @returns {Promise} The refreshed URL or the original URL if no refresh needed - */ -async function refreshS3Url(fileObj, bufferSeconds = 3600) { - if (!fileObj || fileObj.source !== FileSources.s3 || !fileObj.filepath) { - return fileObj?.filepath || ''; - } - - if (!needsRefresh(fileObj.filepath, bufferSeconds)) { - return fileObj.filepath; - } - - try { - const s3Key = extractKeyFromS3Url(fileObj.filepath); - if (!s3Key) { - logger.warn(`Unable to extract S3 key from URL: ${fileObj.filepath}`); - return fileObj.filepath; - } - - const keyParts = s3Key.split('/'); - if (keyParts.length < 3) { - logger.warn(`Invalid S3 key format: ${s3Key}`); - return fileObj.filepath; - } - - const basePath = keyParts[0]; - const userId = keyParts[1]; - const fileName = keyParts.slice(2).join('/'); - - const newUrl = await getS3URL({ - userId, - fileName, - basePath, - }); - - logger.debug(`Refreshed S3 URL for key: ${s3Key}`); - return newUrl; - } catch (error) { - logger.error(`Error refreshing S3 URL: ${error.message}`); - return fileObj.filepath; - } -} - -module.exports = { - saveBufferToS3, - saveURLToS3, - getS3URL, - deleteFileFromS3, - uploadFileToS3, - getS3FileStream, - refreshS3FileUrls, - refreshS3Url, - needsRefresh, - getNewS3URL, - extractKeyFromS3Url, -}; diff --git a/api/server/services/Files/S3/images.js b/api/server/services/Files/S3/images.js deleted file mode 100644 index 9bdae940c3..0000000000 --- a/api/server/services/Files/S3/images.js +++ /dev/null @@ -1,129 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const sharp = require('sharp'); -const { logger } = require('@librechat/data-schemas'); -const { resizeImageBuffer } = require('../images/resize'); -const { updateUser, updateFile } = require('~/models'); -const { saveBufferToS3 } = require('./crud'); - -const defaultBasePath = 'images'; - -/** - * Resizes, converts, and uploads an image file to S3. - * - * @param {Object} params - * @param {import('express').Request} params.req - Express request (expects `user` and `appConfig.imageOutputType`). - * @param {Express.Multer.File} params.file - File object from Multer. - * @param {string} params.file_id - Unique file identifier. - * @param {any} params.endpoint - Endpoint identifier used in image processing. - * @param {string} [params.resolution='high'] - Desired image resolution. - * @param {string} [params.basePath='images'] - Base path in the bucket. - * @returns {Promise<{ filepath: string, bytes: number, width: number, height: number }>} - */ -async function uploadImageToS3({ - req, - file, - file_id, - endpoint, - resolution = 'high', - basePath = defaultBasePath, -}) { - try { - const appConfig = req.config; - const inputFilePath = file.path; - const inputBuffer = await fs.promises.readFile(inputFilePath); - const { - buffer: resizedBuffer, - width, - height, - } = await resizeImageBuffer(inputBuffer, resolution, endpoint); - const extension = path.extname(inputFilePath); - const userId = req.user.id; - - let processedBuffer; - let fileName = `${file_id}__${path.basename(inputFilePath)}`; - const targetExtension = `.${appConfig.imageOutputType}`; - - if (extension.toLowerCase() === targetExtension) { - processedBuffer = resizedBuffer; - } else { - processedBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer(); - fileName = fileName.replace(new RegExp(path.extname(fileName) + '$'), targetExtension); - if (!path.extname(fileName)) { - fileName += targetExtension; - } - } - - const downloadURL = await saveBufferToS3({ - userId, - buffer: processedBuffer, - fileName, - basePath, - }); - await fs.promises.unlink(inputFilePath); - const bytes = Buffer.byteLength(processedBuffer); - return { filepath: downloadURL, bytes, width, height }; - } catch (error) { - logger.error('[uploadImageToS3] Error uploading image to S3:', error.message); - throw error; - } -} - -/** - * Updates a file record and returns its signed URL. - * - * @param {import('express').Request} req - Express request. - * @param {Object} file - File metadata. - * @returns {Promise<[Promise, string]>} - */ -async function prepareImageURLS3(req, file) { - try { - const updatePromise = updateFile({ file_id: file.file_id }); - return Promise.all([updatePromise, file.filepath]); - } catch (error) { - logger.error('[prepareImageURLS3] Error preparing image URL:', error.message); - throw error; - } -} - -/** - * Processes a user's avatar image by uploading it to S3 and updating the user's avatar URL if required. - * - * @param {Object} params - * @param {Buffer} params.buffer - Avatar image buffer. - * @param {string} params.userId - User's unique identifier. - * @param {string} params.manual - 'true' or 'false' flag for manual update. - * @param {string} [params.agentId] - Optional agent ID if this is an agent avatar. - * @param {string} [params.basePath='images'] - Base path in the bucket. - * @returns {Promise} Signed URL of the uploaded avatar. - */ -async function processS3Avatar({ buffer, userId, manual, agentId, basePath = defaultBasePath }) { - try { - const metadata = await sharp(buffer).metadata(); - const extension = metadata.format === 'gif' ? 'gif' : 'png'; - const timestamp = new Date().getTime(); - - /** Unique filename with timestamp and optional agent ID */ - const fileName = agentId - ? `agent-${agentId}-avatar-${timestamp}.${extension}` - : `avatar-${timestamp}.${extension}`; - - const downloadURL = await saveBufferToS3({ userId, buffer, fileName, basePath }); - - // Only update user record if this is a user avatar (manual === 'true') - if (manual === 'true' && !agentId) { - await updateUser(userId, { avatar: downloadURL }); - } - - return downloadURL; - } catch (error) { - logger.error('[processS3Avatar] Error processing S3 avatar:', error.message); - throw error; - } -} - -module.exports = { - uploadImageToS3, - prepareImageURLS3, - processS3Avatar, -}; diff --git a/api/server/services/Files/S3/index.js b/api/server/services/Files/S3/index.js deleted file mode 100644 index 21e2f2ba7d..0000000000 --- a/api/server/services/Files/S3/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const crud = require('./crud'); -const images = require('./images'); - -module.exports = { - ...crud, - ...images, -}; diff --git a/api/server/services/Files/strategies.js b/api/server/services/Files/strategies.js index 25341b5715..47b39cb87b 100644 --- a/api/server/services/Files/strategies.js +++ b/api/server/services/Files/strategies.js @@ -1,6 +1,13 @@ const { FileSources } = require('librechat-data-provider'); const { + getS3URL, + saveURLToS3, parseDocument, + uploadFileToS3, + S3ImageService, + saveBufferToS3, + getS3FileStream, + deleteFileFromS3, uploadMistralOCR, uploadAzureMistralOCR, uploadGoogleVertexMistralOCR, @@ -27,17 +34,18 @@ const { processLocalAvatar, getLocalFileStream, } = require('./Local'); -const { - getS3URL, - saveURLToS3, - saveBufferToS3, - getS3FileStream, - uploadImageToS3, - prepareImageURLS3, - deleteFileFromS3, - processS3Avatar, - uploadFileToS3, -} = require('./S3'); +const { resizeImageBuffer } = require('./images/resize'); +const { updateUser, updateFile } = require('~/models'); + +const s3ImageService = new S3ImageService({ + resizeImageBuffer, + updateUser, + updateFile, +}); + +const uploadImageToS3 = (params) => s3ImageService.uploadImageToS3(params); +const prepareImageURLS3 = (_req, file) => s3ImageService.prepareImageURL(file); +const processS3Avatar = (params) => s3ImageService.processAvatar(params); const { saveBufferToAzure, saveURLToAzure, diff --git a/api/test/server/services/Files/S3/crud.test.js b/api/test/server/services/Files/S3/crud.test.js deleted file mode 100644 index d847a82cf0..0000000000 --- a/api/test/server/services/Files/S3/crud.test.js +++ /dev/null @@ -1,72 +0,0 @@ -const { getS3URL } = require('../../../../../server/services/Files/S3/crud'); - -// Mock AWS SDK -jest.mock('@aws-sdk/client-s3', () => ({ - S3Client: jest.fn(() => ({ - send: jest.fn(), - })), - GetObjectCommand: jest.fn(), -})); - -jest.mock('@aws-sdk/s3-request-presigner', () => ({ - getSignedUrl: jest.fn(), -})); - -jest.mock('../../../../../config', () => ({ - logger: { - error: jest.fn(), - }, -})); - -const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); -const { GetObjectCommand } = require('@aws-sdk/client-s3'); - -describe('S3 crud.js - test only new parameter changes', () => { - beforeEach(() => { - jest.clearAllMocks(); - process.env.AWS_BUCKET_NAME = 'test-bucket'; - }); - - // Test only the new customFilename parameter - it('should include customFilename in response headers when provided', async () => { - getSignedUrl.mockResolvedValue('https://test-presigned-url.com'); - - await getS3URL({ - userId: 'user123', - fileName: 'test.pdf', - customFilename: 'cleaned_filename.pdf', - }); - - // Verify the new ResponseContentDisposition parameter is added to GetObjectCommand - const commandArgs = GetObjectCommand.mock.calls[0][0]; - expect(commandArgs.ResponseContentDisposition).toBe( - 'attachment; filename="cleaned_filename.pdf"', - ); - }); - - // Test only the new contentType parameter - it('should include contentType in response headers when provided', async () => { - getSignedUrl.mockResolvedValue('https://test-presigned-url.com'); - - await getS3URL({ - userId: 'user123', - fileName: 'test.pdf', - contentType: 'application/pdf', - }); - - // Verify the new ResponseContentType parameter is added to GetObjectCommand - const commandArgs = GetObjectCommand.mock.calls[0][0]; - expect(commandArgs.ResponseContentType).toBe('application/pdf'); - }); - - it('should work without new parameters (backward compatibility)', async () => { - getSignedUrl.mockResolvedValue('https://test-presigned-url.com'); - - const result = await getS3URL({ - userId: 'user123', - fileName: 'test.pdf', - }); - - expect(result).toBe('https://test-presigned-url.com'); - }); -}); diff --git a/api/test/services/Files/S3/crud.test.js b/api/test/services/Files/S3/crud.test.js deleted file mode 100644 index c7b46fba4c..0000000000 --- a/api/test/services/Files/S3/crud.test.js +++ /dev/null @@ -1,876 +0,0 @@ -const fs = require('fs'); -const fetch = require('node-fetch'); -const { Readable } = require('stream'); -const { FileSources } = require('librechat-data-provider'); -const { - PutObjectCommand, - GetObjectCommand, - HeadObjectCommand, - DeleteObjectCommand, -} = require('@aws-sdk/client-s3'); -const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); - -// Mock dependencies -jest.mock('fs'); -jest.mock('node-fetch'); -jest.mock('@aws-sdk/s3-request-presigner'); -jest.mock('@aws-sdk/client-s3'); - -jest.mock('@librechat/api', () => ({ - initializeS3: jest.fn(), - deleteRagFile: jest.fn().mockResolvedValue(undefined), - isEnabled: jest.fn((val) => val === 'true'), -})); - -jest.mock('@librechat/data-schemas', () => ({ - logger: { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, -})); - -const { initializeS3, deleteRagFile } = require('@librechat/api'); -const { logger } = require('@librechat/data-schemas'); - -// Set env vars before requiring crud so module-level constants pick them up -process.env.AWS_BUCKET_NAME = 'test-bucket'; -process.env.S3_URL_EXPIRY_SECONDS = '120'; - -const { - saveBufferToS3, - saveURLToS3, - getS3URL, - deleteFileFromS3, - uploadFileToS3, - getS3FileStream, - refreshS3FileUrls, - refreshS3Url, - needsRefresh, - getNewS3URL, - extractKeyFromS3Url, -} = require('~/server/services/Files/S3/crud'); - -describe('S3 CRUD Operations', () => { - let mockS3Client; - - beforeEach(() => { - jest.clearAllMocks(); - - // Setup mock S3 client - mockS3Client = { - send: jest.fn(), - }; - initializeS3.mockReturnValue(mockS3Client); - }); - - afterEach(() => { - delete process.env.S3_URL_EXPIRY_SECONDS; - delete process.env.S3_REFRESH_EXPIRY_MS; - delete process.env.AWS_BUCKET_NAME; - }); - - describe('saveBufferToS3', () => { - it('should upload a buffer to S3 and return a signed URL', async () => { - const mockBuffer = Buffer.from('test data'); - const mockSignedUrl = - 'https://s3.amazonaws.com/test-bucket/images/user123/test.jpg?signature=abc'; - - mockS3Client.send.mockResolvedValue({}); - getSignedUrl.mockResolvedValue(mockSignedUrl); - - const result = await saveBufferToS3({ - userId: 'user123', - buffer: mockBuffer, - fileName: 'test.jpg', - basePath: 'images', - }); - - expect(mockS3Client.send).toHaveBeenCalledWith(expect.any(PutObjectCommand)); - expect(result).toBe(mockSignedUrl); - }); - - it('should use default basePath if not provided', async () => { - const mockBuffer = Buffer.from('test data'); - const mockSignedUrl = - 'https://s3.amazonaws.com/test-bucket/images/user123/test.jpg?signature=abc'; - - mockS3Client.send.mockResolvedValue({}); - getSignedUrl.mockResolvedValue(mockSignedUrl); - - await saveBufferToS3({ - userId: 'user123', - buffer: mockBuffer, - fileName: 'test.jpg', - }); - - expect(getSignedUrl).toHaveBeenCalled(); - }); - - it('should handle S3 upload errors', async () => { - const mockBuffer = Buffer.from('test data'); - const error = new Error('S3 upload failed'); - - mockS3Client.send.mockRejectedValue(error); - - await expect( - saveBufferToS3({ - userId: 'user123', - buffer: mockBuffer, - fileName: 'test.jpg', - }), - ).rejects.toThrow('S3 upload failed'); - - expect(logger.error).toHaveBeenCalledWith( - '[saveBufferToS3] Error uploading buffer to S3:', - 'S3 upload failed', - ); - }); - }); - - describe('getS3URL', () => { - it('should return a signed URL for a file', async () => { - const mockSignedUrl = - 'https://s3.amazonaws.com/test-bucket/images/user123/file.pdf?signature=xyz'; - getSignedUrl.mockResolvedValue(mockSignedUrl); - - const result = await getS3URL({ - userId: 'user123', - fileName: 'file.pdf', - basePath: 'documents', - }); - - expect(result).toBe(mockSignedUrl); - expect(getSignedUrl).toHaveBeenCalledWith( - mockS3Client, - expect.any(GetObjectCommand), - expect.objectContaining({ expiresIn: 120 }), - ); - }); - - it('should add custom filename to Content-Disposition header', async () => { - const mockSignedUrl = - 'https://s3.amazonaws.com/test-bucket/images/user123/file.pdf?signature=xyz'; - getSignedUrl.mockResolvedValue(mockSignedUrl); - - await getS3URL({ - userId: 'user123', - fileName: 'file.pdf', - customFilename: 'custom-name.pdf', - }); - - expect(getSignedUrl).toHaveBeenCalled(); - }); - - it('should add custom content type', async () => { - const mockSignedUrl = - 'https://s3.amazonaws.com/test-bucket/images/user123/file.pdf?signature=xyz'; - getSignedUrl.mockResolvedValue(mockSignedUrl); - - await getS3URL({ - userId: 'user123', - fileName: 'file.pdf', - contentType: 'application/pdf', - }); - - expect(getSignedUrl).toHaveBeenCalled(); - }); - - it('should handle errors when getting signed URL', async () => { - const error = new Error('Failed to sign URL'); - getSignedUrl.mockRejectedValue(error); - - await expect( - getS3URL({ - userId: 'user123', - fileName: 'file.pdf', - }), - ).rejects.toThrow('Failed to sign URL'); - - expect(logger.error).toHaveBeenCalledWith( - '[getS3URL] Error getting signed URL from S3:', - 'Failed to sign URL', - ); - }); - }); - - describe('saveURLToS3', () => { - it('should fetch a file from URL and save to S3', async () => { - const mockBuffer = Buffer.from('downloaded data'); - const mockResponse = { - buffer: jest.fn().mockResolvedValue(mockBuffer), - }; - const mockSignedUrl = - 'https://s3.amazonaws.com/test-bucket/images/user123/downloaded.jpg?signature=abc'; - - fetch.mockResolvedValue(mockResponse); - mockS3Client.send.mockResolvedValue({}); - getSignedUrl.mockResolvedValue(mockSignedUrl); - - const result = await saveURLToS3({ - userId: 'user123', - URL: 'https://example.com/image.jpg', - fileName: 'downloaded.jpg', - }); - - expect(fetch).toHaveBeenCalledWith('https://example.com/image.jpg'); - expect(mockS3Client.send).toHaveBeenCalled(); - expect(result).toBe(mockSignedUrl); - }); - - it('should handle fetch errors', async () => { - const error = new Error('Network error'); - fetch.mockRejectedValue(error); - - await expect( - saveURLToS3({ - userId: 'user123', - URL: 'https://example.com/image.jpg', - fileName: 'downloaded.jpg', - }), - ).rejects.toThrow('Network error'); - - expect(logger.error).toHaveBeenCalled(); - }); - }); - - describe('deleteFileFromS3', () => { - const mockReq = { - user: { id: 'user123' }, - }; - - it('should delete a file from S3', async () => { - const mockFile = { - filepath: 'https://s3.amazonaws.com/test-bucket/images/user123/file.jpg', - file_id: 'file123', - }; - - // Mock HeadObject to verify file exists - mockS3Client.send - .mockResolvedValueOnce({}) // First HeadObject - exists - .mockResolvedValueOnce({}) // DeleteObject - .mockRejectedValueOnce({ name: 'NotFound' }); // Second HeadObject - deleted - - await deleteFileFromS3(mockReq, mockFile); - - expect(deleteRagFile).toHaveBeenCalledWith({ userId: 'user123', file: mockFile }); - expect(mockS3Client.send).toHaveBeenCalledWith(expect.any(HeadObjectCommand)); - expect(mockS3Client.send).toHaveBeenCalledWith(expect.any(DeleteObjectCommand)); - }); - - it('should handle file not found gracefully', async () => { - const mockFile = { - filepath: 'https://s3.amazonaws.com/test-bucket/images/user123/nonexistent.jpg', - file_id: 'file123', - }; - - mockS3Client.send.mockRejectedValue({ name: 'NotFound' }); - - await deleteFileFromS3(mockReq, mockFile); - - expect(logger.warn).toHaveBeenCalled(); - }); - - it('should throw error if user ID does not match', async () => { - const mockFile = { - filepath: 'https://s3.amazonaws.com/test-bucket/images/different-user/file.jpg', - file_id: 'file123', - }; - - await expect(deleteFileFromS3(mockReq, mockFile)).rejects.toThrow('User ID mismatch'); - expect(logger.error).toHaveBeenCalled(); - }); - - it('should handle NoSuchKey error', async () => { - const mockFile = { - filepath: 'https://s3.amazonaws.com/test-bucket/images/user123/file.jpg', - file_id: 'file123', - }; - - mockS3Client.send - .mockResolvedValueOnce({}) // HeadObject - exists - .mockRejectedValueOnce({ code: 'NoSuchKey' }); // DeleteObject fails - - await deleteFileFromS3(mockReq, mockFile); - - expect(logger.debug).toHaveBeenCalled(); - }); - }); - - describe('uploadFileToS3', () => { - const mockReq = { - user: { id: 'user123' }, - }; - - it('should upload a file from disk to S3', async () => { - const mockFile = { - path: '/tmp/upload.jpg', - originalname: 'photo.jpg', - }; - const mockStats = { size: 1024 }; - const mockSignedUrl = - 'https://s3.amazonaws.com/test-bucket/images/user123/file123__photo.jpg?signature=xyz'; - - fs.promises = { stat: jest.fn().mockResolvedValue(mockStats) }; - fs.createReadStream = jest.fn().mockReturnValue(new Readable()); - mockS3Client.send.mockResolvedValue({}); - getSignedUrl.mockResolvedValue(mockSignedUrl); - - const result = await uploadFileToS3({ - req: mockReq, - file: mockFile, - file_id: 'file123', - basePath: 'images', - }); - - expect(result).toEqual({ - filepath: mockSignedUrl, - bytes: 1024, - }); - expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload.jpg'); - expect(mockS3Client.send).toHaveBeenCalledWith(expect.any(PutObjectCommand)); - }); - - it('should handle upload errors and clean up temp file', async () => { - const mockFile = { - path: '/tmp/upload.jpg', - originalname: 'photo.jpg', - }; - const error = new Error('Upload failed'); - - fs.promises = { - stat: jest.fn().mockResolvedValue({ size: 1024 }), - unlink: jest.fn().mockResolvedValue(), - }; - fs.createReadStream = jest.fn().mockReturnValue(new Readable()); - mockS3Client.send.mockRejectedValue(error); - - await expect( - uploadFileToS3({ - req: mockReq, - file: mockFile, - file_id: 'file123', - }), - ).rejects.toThrow('Upload failed'); - - expect(logger.error).toHaveBeenCalledWith( - '[uploadFileToS3] Error streaming file to S3:', - error, - ); - }); - }); - - describe('getS3FileStream', () => { - it('should return a readable stream for a file', async () => { - const mockStream = new Readable(); - const mockResponse = { Body: mockStream }; - - mockS3Client.send.mockResolvedValue(mockResponse); - - const result = await getS3FileStream( - {}, - 'https://s3.amazonaws.com/test-bucket/images/user123/file.pdf', - ); - - expect(result).toBe(mockStream); - expect(mockS3Client.send).toHaveBeenCalledWith(expect.any(GetObjectCommand)); - }); - - it('should handle errors when retrieving stream', async () => { - const error = new Error('Stream error'); - mockS3Client.send.mockRejectedValue(error); - - await expect(getS3FileStream({}, 'images/user123/file.pdf')).rejects.toThrow('Stream error'); - expect(logger.error).toHaveBeenCalled(); - }); - }); - - describe('needsRefresh', () => { - it('should return false for non-signed URLs', () => { - const url = 'https://example.com/proxy/file.jpg'; - const result = needsRefresh(url, 3600); - expect(result).toBe(false); - }); - - it('should return true for expired signed URLs', () => { - const now = new Date(); - const past = new Date(now.getTime() - 3600 * 1000); // 1 hour ago - const dateStr = past - .toISOString() - .replace(/[-:]/g, '') - .replace(/\.\d{3}/, ''); - - const url = `https://s3.amazonaws.com/bucket/key?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`; - const result = needsRefresh(url, 60); - expect(result).toBe(true); - }); - - it('should return false for URLs that are not close to expiration', () => { - const now = new Date(); - const recent = new Date(now.getTime() - 10 * 1000); // 10 seconds ago - const dateStr = recent - .toISOString() - .replace(/[-:]/g, '') - .replace(/\.\d{3}/, ''); - - const url = `https://s3.amazonaws.com/bucket/key?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=7200`; - const result = needsRefresh(url, 60); - expect(result).toBe(false); - }); - - it('should use custom refresh expiry when S3_REFRESH_EXPIRY_MS is set', () => { - process.env.S3_REFRESH_EXPIRY_MS = '30000'; // 30 seconds - - const now = new Date(); - const recent = new Date(now.getTime() - 31 * 1000); // 31 seconds ago - const dateStr = recent - .toISOString() - .replace(/[-:]/g, '') - .replace(/\.\d{3}/, ''); - - const url = `https://s3.amazonaws.com/bucket/key?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=7200`; - - // Need to reload the module to pick up the env var change - jest.resetModules(); - const { needsRefresh: needsRefreshReloaded } = require('~/server/services/Files/S3/crud'); - - const result = needsRefreshReloaded(url, 60); - expect(result).toBe(true); - }); - - it('should return true for malformed URLs', () => { - const url = 'not-a-valid-url'; - const result = needsRefresh(url, 3600); - expect(result).toBe(true); - }); - }); - - describe('getNewS3URL', () => { - it('should generate a new URL from an existing S3 URL', async () => { - const currentURL = - 'https://s3.amazonaws.com/test-bucket/images/user123/file.jpg?signature=old'; - const newURL = 'https://s3.amazonaws.com/test-bucket/images/user123/file.jpg?signature=new'; - - getSignedUrl.mockResolvedValue(newURL); - - const result = await getNewS3URL(currentURL); - - expect(result).toBe(newURL); - expect(getSignedUrl).toHaveBeenCalled(); - }); - - it('should return undefined for invalid URLs', async () => { - const result = await getNewS3URL('invalid-url'); - expect(result).toBeUndefined(); - }); - - it('should handle errors gracefully', async () => { - const currentURL = 'https://s3.amazonaws.com/test-bucket/images/user123/file.jpg'; - getSignedUrl.mockRejectedValue(new Error('Failed')); - - const result = await getNewS3URL(currentURL); - - expect(result).toBeUndefined(); - expect(logger.error).toHaveBeenCalledWith('Error getting new S3 URL:', expect.any(Error)); - }); - - it('should construct GetObjectCommand with correct key (no bucket name duplication)', async () => { - const currentURL = - 'https://s3.amazonaws.com/my-bucket/images/user123/file.jpg?X-Amz-Signature=old'; - getSignedUrl.mockResolvedValue( - 'https://s3.amazonaws.com/test-bucket/images/user123/file.jpg?signature=new', - ); - - await getNewS3URL(currentURL); - - expect(GetObjectCommand).toHaveBeenCalledWith( - expect.objectContaining({ Key: 'images/user123/file.jpg' }), - ); - }); - }); - - describe('refreshS3FileUrls', () => { - it('should refresh expired URLs for multiple files', async () => { - const now = new Date(); - const past = new Date(now.getTime() - 3600 * 1000); - const dateStr = past - .toISOString() - .replace(/[-:]/g, '') - .replace(/\.\d{3}/, ''); - - const files = [ - { - file_id: 'file1', - source: FileSources.s3, - filepath: `https://s3.amazonaws.com/bucket/images/user123/file1.jpg?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, - }, - { - file_id: 'file2', - source: FileSources.s3, - filepath: `https://s3.amazonaws.com/bucket/images/user123/file2.jpg?X-Amz-Signature=def&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, - }, - ]; - - const newURL1 = 'https://s3.amazonaws.com/bucket/images/user123/file1.jpg?signature=new1'; - const newURL2 = 'https://s3.amazonaws.com/bucket/images/user123/file2.jpg?signature=new2'; - - getSignedUrl.mockResolvedValueOnce(newURL1).mockResolvedValueOnce(newURL2); - - const mockBatchUpdate = jest.fn().mockResolvedValue(); - - const result = await refreshS3FileUrls(files, mockBatchUpdate, 60); - - expect(result[0].filepath).toBe(newURL1); - expect(result[1].filepath).toBe(newURL2); - expect(mockBatchUpdate).toHaveBeenCalledWith([ - { file_id: 'file1', filepath: newURL1 }, - { file_id: 'file2', filepath: newURL2 }, - ]); - }); - - it('should skip non-S3 files', async () => { - const files = [ - { - file_id: 'file1', - source: 'local', - filepath: '/local/path/file.jpg', - }, - ]; - - const mockBatchUpdate = jest.fn(); - - const result = await refreshS3FileUrls(files, mockBatchUpdate); - - expect(result).toEqual(files); - expect(mockBatchUpdate).not.toHaveBeenCalled(); - }); - - it('should handle empty or invalid input', async () => { - const mockBatchUpdate = jest.fn(); - - const result1 = await refreshS3FileUrls(null, mockBatchUpdate); - expect(result1).toBe(null); - - const result2 = await refreshS3FileUrls([], mockBatchUpdate); - expect(result2).toEqual([]); - - expect(mockBatchUpdate).not.toHaveBeenCalled(); - }); - - it('should handle errors for individual files gracefully', async () => { - const now = new Date(); - const past = new Date(now.getTime() - 3600 * 1000); - const dateStr = past - .toISOString() - .replace(/[-:]/g, '') - .replace(/\.\d{3}/, ''); - - const files = [ - { - file_id: 'file1', - source: FileSources.s3, - filepath: `https://s3.amazonaws.com/bucket/images/user123/file1.jpg?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, - }, - ]; - - getSignedUrl.mockRejectedValue(new Error('Failed to refresh')); - const mockBatchUpdate = jest.fn(); - - await refreshS3FileUrls(files, mockBatchUpdate, 60); - - expect(logger.error).toHaveBeenCalledWith('Error getting new S3 URL:', expect.any(Error)); - expect(mockBatchUpdate).not.toHaveBeenCalled(); - }); - }); - - describe('refreshS3Url', () => { - it('should refresh an expired S3 URL', async () => { - const now = new Date(); - const past = new Date(now.getTime() - 3600 * 1000); - const dateStr = past - .toISOString() - .replace(/[-:]/g, '') - .replace(/\.\d{3}/, ''); - - const fileObj = { - source: FileSources.s3, - filepath: `https://s3.amazonaws.com/bucket/images/user123/file.jpg?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, - }; - - const newURL = 'https://s3.amazonaws.com/bucket/images/user123/file.jpg?signature=new'; - getSignedUrl.mockResolvedValue(newURL); - - const result = await refreshS3Url(fileObj, 60); - - expect(result).toBe(newURL); - }); - - it('should return original URL if not expired', async () => { - const fileObj = { - source: FileSources.s3, - filepath: 'https://example.com/proxy/file.jpg', - }; - - const result = await refreshS3Url(fileObj, 3600); - - expect(result).toBe(fileObj.filepath); - expect(getSignedUrl).not.toHaveBeenCalled(); - }); - - it('should return empty string for null input', async () => { - const result = await refreshS3Url(null); - expect(result).toBe(''); - }); - - it('should return original URL for non-S3 files', async () => { - const fileObj = { - source: 'local', - filepath: '/local/path/file.jpg', - }; - - const result = await refreshS3Url(fileObj); - - expect(result).toBe(fileObj.filepath); - }); - - it('should handle errors and return original URL', async () => { - const now = new Date(); - const past = new Date(now.getTime() - 3600 * 1000); - const dateStr = past - .toISOString() - .replace(/[-:]/g, '') - .replace(/\.\d{3}/, ''); - - const fileObj = { - source: FileSources.s3, - filepath: `https://s3.amazonaws.com/bucket/images/user123/file.jpg?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, - }; - - getSignedUrl.mockRejectedValue(new Error('Refresh failed')); - - const result = await refreshS3Url(fileObj, 60); - - expect(result).toBe(fileObj.filepath); - expect(logger.error).toHaveBeenCalled(); - }); - }); - - describe('extractKeyFromS3Url', () => { - it('should extract key from a full S3 URL', () => { - const url = 'https://s3.amazonaws.com/test-bucket/images/user123/file.jpg'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('images/user123/file.jpg'); - }); - - it('should extract key from a signed S3 URL with query parameters', () => { - const url = - 'https://s3.amazonaws.com/test-bucket/documents/user456/report.pdf?X-Amz-Signature=abc123&X-Amz-Date=20260107'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('documents/user456/report.pdf'); - }); - - it('should extract key from S3 URL with different domain format', () => { - const url = 'https://test-bucket.s3.amazonaws.com/uploads/user789/image.png'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('uploads/user789/image.png'); - }); - - it('should return key as-is if already properly formatted (3+ parts, no http)', () => { - const key = 'images/user123/file.jpg'; - const result = extractKeyFromS3Url(key); - expect(result).toBe('images/user123/file.jpg'); - }); - - it('should handle key with leading slash by removing it', () => { - const key = '/images/user123/file.jpg'; - const result = extractKeyFromS3Url(key); - expect(result).toBe('images/user123/file.jpg'); - }); - - it('should handle simple key without slashes', () => { - const key = 'simple-file.txt'; - const result = extractKeyFromS3Url(key); - expect(result).toBe('simple-file.txt'); - }); - - it('should handle key with only two parts', () => { - const key = 'folder/file.txt'; - const result = extractKeyFromS3Url(key); - expect(result).toBe('folder/file.txt'); - }); - - it('should throw error for empty input', () => { - expect(() => extractKeyFromS3Url('')).toThrow('Invalid input: URL or key is empty'); - }); - - it('should throw error for null input', () => { - expect(() => extractKeyFromS3Url(null)).toThrow('Invalid input: URL or key is empty'); - }); - - it('should throw error for undefined input', () => { - expect(() => extractKeyFromS3Url(undefined)).toThrow('Invalid input: URL or key is empty'); - }); - - it('should handle URLs with encoded characters', () => { - const url = 'https://s3.amazonaws.com/test-bucket/images/user123/my%20file%20name.jpg'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('images/user123/my%20file%20name.jpg'); - }); - - it('should handle deep nested paths', () => { - const url = 'https://s3.amazonaws.com/bucket/a/b/c/d/e/f/file.jpg'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('a/b/c/d/e/f/file.jpg'); - }); - - it('should log debug message when extracting from URL', () => { - const url = 'https://s3.amazonaws.com/bucket/images/user123/file.jpg'; - extractKeyFromS3Url(url); - expect(logger.debug).toHaveBeenCalledWith( - expect.stringContaining('[extractKeyFromS3Url] fileUrlOrKey:'), - ); - }); - - it('should log fallback debug message for non-URL input', () => { - const key = 'simple-file.txt'; - extractKeyFromS3Url(key); - expect(logger.debug).toHaveBeenCalledWith( - expect.stringContaining('[extractKeyFromS3Url] FALLBACK'), - ); - }); - - it('should handle valid URLs that contain only a bucket', () => { - const url = 'https://s3.amazonaws.com/test-bucket/'; - const result = extractKeyFromS3Url(url); - expect(logger.warn).toHaveBeenCalledWith( - expect.stringContaining( - '[extractKeyFromS3Url] Extracted key is empty after removing bucket name from URL: https://s3.amazonaws.com/test-bucket/', - ), - ); - expect(result).toBe(''); - }); - - it('should handle invalid URLs that contain only a bucket', () => { - const url = 'https://s3.amazonaws.com/test-bucket'; - const result = extractKeyFromS3Url(url); - expect(logger.warn).toHaveBeenCalledWith( - expect.stringContaining( - '[extractKeyFromS3Url] Unable to extract key from path-style URL: https://s3.amazonaws.com/test-bucket', - ), - ); - expect(result).toBe(''); - }); - - // https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html - - // Path-style requests - // https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access - // https://s3.region-code.amazonaws.com/bucket-name/key-name - it('should handle formatted according to Path-style regional endpoint', () => { - const url = 'https://s3.us-west-2.amazonaws.com/amzn-s3-demo-bucket1/dogs/puppy.jpg'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('dogs/puppy.jpg'); - }); - - // virtual host style - // https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access - // https://bucket-name.s3.region-code.amazonaws.com/key-name - it('should handle formatted according to Virtual-hosted–style Regional endpoint', () => { - const url = 'https://amzn-s3-demo-bucket1.s3.us-west-2.amazonaws.com/dogs/puppy.png'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('dogs/puppy.png'); - }); - - // Legacy endpoints - // https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#VirtualHostingBackwardsCompatibility - - // s3‐Region - // https://bucket-name.s3-region-code.amazonaws.com - it('should handle formatted according to s3‐Region', () => { - const url = 'https://amzn-s3-demo-bucket1.s3-us-west-2.amazonaws.com/puppy.png'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('puppy.png'); - - const testcase2 = 'https://amzn-s3-demo-bucket1.s3-us-west-2.amazonaws.com/cats/kitten.png'; - const result2 = extractKeyFromS3Url(testcase2); - expect(result2).toBe('cats/kitten.png'); - }); - - // Legacy global endpoint - // bucket-name.s3.amazonaws.com - it('should handle formatted according to Legacy global endpoint', () => { - const url = 'https://amzn-s3-demo-bucket1.s3.amazonaws.com/dogs/puppy.png'; - const result = extractKeyFromS3Url(url); - expect(result).toBe('dogs/puppy.png'); - }); - - it('should handle malformed URL and log error', () => { - const malformedUrl = 'https://invalid url with spaces.com/key'; - const result = extractKeyFromS3Url(malformedUrl); - - expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining('[extractKeyFromS3Url] Error parsing URL:'), - ); - expect(logger.error).toHaveBeenCalledWith(expect.stringContaining(malformedUrl)); - - expect(result).toBe(malformedUrl); - }); - - it('should return empty string for regional path-style URL with only bucket (no key)', () => { - const url = 'https://s3.us-west-2.amazonaws.com/my-bucket'; - const result = extractKeyFromS3Url(url); - expect(result).toBe(''); - expect(logger.warn).toHaveBeenCalledWith( - expect.stringContaining('[extractKeyFromS3Url] Unable to extract key from path-style URL:'), - ); - }); - - it('should not log error when given a plain S3 key (non-URL input)', () => { - extractKeyFromS3Url('images/user123/file.jpg'); - expect(logger.error).not.toHaveBeenCalled(); - }); - - it('should strip bucket from custom endpoint URLs (MinIO, R2, etc.) using bucketName', () => { - // bucketName is the module-level const 'test-bucket', set before require at top of file - expect( - extractKeyFromS3Url('https://minio.example.com/test-bucket/images/user123/file.jpg'), - ).toBe('images/user123/file.jpg'); - expect( - extractKeyFromS3Url( - 'https://abc123.r2.cloudflarestorage.com/test-bucket/images/user123/avatar.png', - ), - ).toBe('images/user123/avatar.png'); - }); - - it('should use endpoint base path when AWS_ENDPOINT_URL and AWS_FORCE_PATH_STYLE are set', () => { - process.env.AWS_BUCKET_NAME = 'test-bucket'; - process.env.AWS_ENDPOINT_URL = 'https://minio.example.com'; - process.env.AWS_FORCE_PATH_STYLE = 'true'; - jest.resetModules(); - const { extractKeyFromS3Url: fn } = require('~/server/services/Files/S3/crud'); - - expect(fn('https://minio.example.com/test-bucket/images/user123/file.jpg')).toBe( - 'images/user123/file.jpg', - ); - - delete process.env.AWS_ENDPOINT_URL; - delete process.env.AWS_FORCE_PATH_STYLE; - }); - - it('should handle endpoint with a base path', () => { - process.env.AWS_BUCKET_NAME = 'test-bucket'; - process.env.AWS_ENDPOINT_URL = 'https://example.com/storage/'; - process.env.AWS_FORCE_PATH_STYLE = 'true'; - jest.resetModules(); - const { extractKeyFromS3Url: fn } = require('~/server/services/Files/S3/crud'); - - expect(fn('https://example.com/storage/test-bucket/images/user123/file.jpg')).toBe( - 'images/user123/file.jpg', - ); - - delete process.env.AWS_ENDPOINT_URL; - delete process.env.AWS_FORCE_PATH_STYLE; - }); - }); -}); diff --git a/package-lock.json b/package-lock.json index 09b994d719..aad4e24fda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -329,44 +329,6 @@ "url": "https://github.com/sponsors/panva" } }, - "api/node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, "api/node_modules/strtok3": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", @@ -19156,6 +19118,34 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@smithy/abort-controller": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", @@ -21155,6 +21145,23 @@ "@types/send": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -22444,6 +22451,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sdk-client-mock": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz", + "integrity": "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinon": "^17.0.3", + "sinon": "^18.0.1", + "tslib": "^2.1.0" + } + }, "node_modules/axe-core": { "version": "4.10.2", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", @@ -31321,6 +31340,13 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -34307,6 +34333,30 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -39857,6 +39907,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -39990,6 +40079,45 @@ "integrity": "sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==", "license": "MIT" }, + "node_modules/sinon": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz", + "integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -43831,6 +43959,7 @@ "@types/react": "^18.2.18", "@types/winston": "^2.4.4", "@types/yauzl": "^2.10.3", + "aws-sdk-client-mock": "^4.1.0", "jest": "^30.2.0", "jest-junit": "^16.0.0", "jszip": "^3.10.1", @@ -43883,6 +44012,7 @@ "node-fetch": "2.7.0", "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", + "sharp": "^0.33.5", "undici": "^7.24.1", "yauzl": "^3.2.1", "zod": "^3.22.4" diff --git a/packages/api/package.json b/packages/api/package.json index 9ca7f9f865..71bb27a3c4 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -25,6 +25,7 @@ "test:cache-integration:mcp": "jest --testPathPatterns=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false", "test:cache-integration:stream": "jest --testPathPatterns=\"src/stream/.*\\.stream_integration\\.spec\\.ts$\" --coverage=false --runInBand --forceExit", "test:cache-integration": "npm run test:cache-integration:core && npm run test:cache-integration:cluster && npm run test:cache-integration:mcp && npm run test:cache-integration:stream", + "test:s3-integration": "jest --testPathPatterns=\"src/storage/s3/.*\\.s3_integration\\.spec\\.ts$\" --coverage=false --runInBand", "verify": "npm run test:ci", "b:clean": "bun run rimraf dist", "b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs", @@ -65,6 +66,7 @@ "@types/react": "^18.2.18", "@types/winston": "^2.4.4", "@types/yauzl": "^2.10.3", + "aws-sdk-client-mock": "^4.1.0", "jest": "^30.2.0", "jszip": "^3.10.1", "jest-junit": "^16.0.0", @@ -120,6 +122,7 @@ "node-fetch": "2.7.0", "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", + "sharp": "^0.33.5", "undici": "^7.24.1", "yauzl": "^3.2.1", "zod": "^3.22.4" diff --git a/packages/api/src/cdn/__tests__/s3.test.ts b/packages/api/src/cdn/__tests__/s3.test.ts index 048c652a45..9a522ecc4f 100644 --- a/packages/api/src/cdn/__tests__/s3.test.ts +++ b/packages/api/src/cdn/__tests__/s3.test.ts @@ -101,6 +101,14 @@ describe('initializeS3', () => { ); }); + it('should throw when AWS_BUCKET_NAME is not set', async () => { + delete process.env.AWS_BUCKET_NAME; + const { initializeS3 } = await load(); + expect(() => initializeS3()).toThrow( + '[S3] AWS_BUCKET_NAME environment variable is required for S3 operations.', + ); + }); + it('should return the same instance on subsequent calls', async () => { const { MockS3Client, initializeS3 } = await load(); const first = initializeS3(); diff --git a/packages/api/src/cdn/s3.ts b/packages/api/src/cdn/s3.ts index f6f8527ce4..c2d0e4d1eb 100644 --- a/packages/api/src/cdn/s3.ts +++ b/packages/api/src/cdn/s3.ts @@ -25,6 +25,13 @@ export const initializeS3 = (): S3Client | null => { return null; } + if (!process.env.AWS_BUCKET_NAME) { + throw new Error( + '[S3] AWS_BUCKET_NAME environment variable is required for S3 operations. ' + + 'Please set this environment variable to enable S3 storage.', + ); + } + // Read the custom endpoint if provided. const endpoint = process.env.AWS_ENDPOINT_URL; const accessKeyId = process.env.AWS_ACCESS_KEY_ID; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 687ee7aa49..ef32e7b6b0 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -37,6 +37,8 @@ export * from './prompts'; export * from './endpoints'; /* Files */ export * from './files'; +/* Storage */ +export * from './storage'; /* Tools */ export * from './tools'; /* web search */ diff --git a/packages/api/src/storage/index.ts b/packages/api/src/storage/index.ts new file mode 100644 index 0000000000..ebd7bd63a9 --- /dev/null +++ b/packages/api/src/storage/index.ts @@ -0,0 +1,2 @@ +export * from './s3'; +export * from './types'; diff --git a/packages/api/src/storage/s3/__tests__/crud.test.ts b/packages/api/src/storage/s3/__tests__/crud.test.ts new file mode 100644 index 0000000000..46e66541ec --- /dev/null +++ b/packages/api/src/storage/s3/__tests__/crud.test.ts @@ -0,0 +1,770 @@ +import fs from 'fs'; +import { Readable } from 'stream'; +import { mockClient } from 'aws-sdk-client-mock'; +import { sdkStreamMixin } from '@smithy/util-stream'; +import { FileSources } from 'librechat-data-provider'; +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + HeadObjectCommand, + DeleteObjectCommand, +} from '@aws-sdk/client-s3'; +import type { TFile } from 'librechat-data-provider'; +import type { S3FileRef } from '~/storage/types'; +import type { ServerRequest } from '~/types'; + +const s3Mock = mockClient(S3Client); + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + promises: { + stat: jest.fn(), + unlink: jest.fn(), + }, + createReadStream: jest.fn(), +})); + +jest.mock('@aws-sdk/s3-request-presigner', () => ({ + getSignedUrl: jest.fn().mockResolvedValue('https://bucket.s3.amazonaws.com/test-key?signed=true'), +})); + +jest.mock('~/files', () => ({ + deleteRagFile: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { deleteRagFile } from '~/files'; +import { logger } from '@librechat/data-schemas'; + +describe('S3 CRUD', () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeAll(() => { + originalEnv = { ...process.env }; + process.env.AWS_REGION = 'us-east-1'; + process.env.AWS_BUCKET_NAME = 'test-bucket'; + process.env.S3_URL_EXPIRY_SECONDS = '120'; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + beforeEach(() => { + s3Mock.reset(); + s3Mock.on(PutObjectCommand).resolves({}); + s3Mock.on(DeleteObjectCommand).resolves({}); + + const stream = new Readable(); + stream.push('test content'); + stream.push(null); + const sdkStream = sdkStreamMixin(stream); + s3Mock.on(GetObjectCommand).resolves({ Body: sdkStream }); + + jest.clearAllMocks(); + }); + + describe('getS3Key', () => { + it('constructs key from basePath, userId, and fileName', async () => { + const { getS3Key } = await import('../crud'); + const key = getS3Key('images', 'user123', 'file.png'); + expect(key).toBe('images/user123/file.png'); + }); + + it('handles nested file names', async () => { + const { getS3Key } = await import('../crud'); + const key = getS3Key('files', 'user456', 'folder/subfolder/doc.pdf'); + expect(key).toBe('files/user456/folder/subfolder/doc.pdf'); + }); + + it('throws if basePath contains a slash', async () => { + const { getS3Key } = await import('../crud'); + expect(() => getS3Key('a/b', 'user123', 'file.png')).toThrow( + '[getS3Key] basePath must not contain slashes: "a/b"', + ); + }); + }); + + describe('saveBufferToS3', () => { + it('uploads buffer and returns signed URL', async () => { + const { saveBufferToS3 } = await import('../crud'); + const result = await saveBufferToS3({ + userId: 'user123', + buffer: Buffer.from('test'), + fileName: 'test.txt', + basePath: 'files', + }); + expect(result).toContain('signed=true'); + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + }); + + it('calls PutObjectCommand with correct parameters', async () => { + const { saveBufferToS3 } = await import('../crud'); + await saveBufferToS3({ + userId: 'user123', + buffer: Buffer.from('test content'), + fileName: 'document.pdf', + basePath: 'documents', + }); + + const calls = s3Mock.commandCalls(PutObjectCommand); + expect(calls[0].args[0].input).toEqual({ + Bucket: 'test-bucket', + Key: 'documents/user123/document.pdf', + Body: Buffer.from('test content'), + }); + }); + + it('uses default basePath if not provided', async () => { + const { saveBufferToS3 } = await import('../crud'); + await saveBufferToS3({ + userId: 'user123', + buffer: Buffer.from('test'), + fileName: 'test.txt', + }); + + const calls = s3Mock.commandCalls(PutObjectCommand); + expect(calls[0].args[0].input.Key).toBe('images/user123/test.txt'); + }); + + it('handles S3 upload errors', async () => { + s3Mock.on(PutObjectCommand).rejects(new Error('S3 upload failed')); + + const { saveBufferToS3 } = await import('../crud'); + await expect( + saveBufferToS3({ + userId: 'user123', + buffer: Buffer.from('test'), + fileName: 'test.txt', + }), + ).rejects.toThrow('S3 upload failed'); + + expect(logger.error).toHaveBeenCalledWith( + '[saveBufferToS3] Error uploading buffer to S3:', + 'S3 upload failed', + ); + }); + }); + + describe('getS3URL', () => { + it('returns signed URL', async () => { + const { getS3URL } = await import('../crud'); + const result = await getS3URL({ + userId: 'user123', + fileName: 'test.txt', + basePath: 'files', + }); + expect(result).toContain('signed=true'); + }); + + it('adds custom filename to Content-Disposition header', async () => { + const { getS3URL } = await import('../crud'); + await getS3URL({ + userId: 'user123', + fileName: 'test.pdf', + customFilename: 'custom-name.pdf', + }); + + expect(getSignedUrl).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + input: expect.objectContaining({ + ResponseContentDisposition: 'attachment; filename="custom-name.pdf"', + }), + }), + expect.anything(), + ); + }); + + it('adds custom content type', async () => { + const { getS3URL } = await import('../crud'); + await getS3URL({ + userId: 'user123', + fileName: 'test.pdf', + contentType: 'application/pdf', + }); + + expect(getSignedUrl).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + input: expect.objectContaining({ + ResponseContentType: 'application/pdf', + }), + }), + expect.anything(), + ); + }); + + it('handles errors when getting signed URL', async () => { + (getSignedUrl as jest.Mock).mockRejectedValueOnce(new Error('Failed to sign URL')); + + const { getS3URL } = await import('../crud'); + await expect( + getS3URL({ + userId: 'user123', + fileName: 'file.pdf', + }), + ).rejects.toThrow('Failed to sign URL'); + + expect(logger.error).toHaveBeenCalledWith( + '[getS3URL] Error getting signed URL from S3:', + 'Failed to sign URL', + ); + }); + }); + + describe('saveURLToS3', () => { + beforeEach(() => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)), + }) as unknown as typeof fetch; + }); + + it('fetches file from URL and saves to S3', async () => { + const { saveURLToS3 } = await import('../crud'); + const result = await saveURLToS3({ + userId: 'user123', + URL: 'https://example.com/image.jpg', + fileName: 'downloaded.jpg', + }); + + expect(global.fetch).toHaveBeenCalledWith('https://example.com/image.jpg'); + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + expect(result).toContain('signed=true'); + }); + + it('throws error on non-ok response', async () => { + (global.fetch as unknown as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)), + }); + + const { saveURLToS3 } = await import('../crud'); + await expect( + saveURLToS3({ + userId: 'user123', + URL: 'https://example.com/missing.jpg', + fileName: 'missing.jpg', + }), + ).rejects.toThrow('Failed to fetch URL'); + }); + + it('handles fetch errors', async () => { + (global.fetch as unknown as jest.Mock).mockRejectedValueOnce(new Error('Network error')); + + const { saveURLToS3 } = await import('../crud'); + await expect( + saveURLToS3({ + userId: 'user123', + URL: 'https://example.com/image.jpg', + fileName: 'downloaded.jpg', + }), + ).rejects.toThrow('Network error'); + + expect(logger.error).toHaveBeenCalled(); + }); + }); + + describe('deleteFileFromS3', () => { + const mockReq = { user: { id: 'user123' } } as ServerRequest; + + it('deletes a file from S3', async () => { + const mockFile = { + filepath: 'https://bucket.s3.amazonaws.com/images/user123/file.jpg', + file_id: 'file123', + } as TFile; + + s3Mock.on(HeadObjectCommand).resolvesOnce({}); + + const { deleteFileFromS3 } = await import('../crud'); + await deleteFileFromS3(mockReq, mockFile); + + expect(deleteRagFile).toHaveBeenCalledWith({ userId: 'user123', file: mockFile }); + expect(s3Mock.commandCalls(HeadObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(DeleteObjectCommand)).toHaveLength(1); + }); + + it('handles file not found gracefully and cleans up RAG', async () => { + const mockFile = { + filepath: 'https://bucket.s3.amazonaws.com/images/user123/nonexistent.jpg', + file_id: 'file123', + } as TFile; + + s3Mock.on(HeadObjectCommand).rejects({ name: 'NotFound' }); + + const { deleteFileFromS3 } = await import('../crud'); + await deleteFileFromS3(mockReq, mockFile); + + expect(logger.warn).toHaveBeenCalled(); + expect(deleteRagFile).toHaveBeenCalledWith({ userId: 'user123', file: mockFile }); + expect(s3Mock.commandCalls(DeleteObjectCommand)).toHaveLength(0); + }); + + it('throws error if user ID does not match', async () => { + const mockFile = { + filepath: 'https://bucket.s3.amazonaws.com/images/different-user/file.jpg', + file_id: 'file123', + } as TFile; + + const { deleteFileFromS3 } = await import('../crud'); + await expect(deleteFileFromS3(mockReq, mockFile)).rejects.toThrow('User ID mismatch'); + expect(logger.error).toHaveBeenCalled(); + }); + + it('handles NoSuchKey error without calling deleteRagFile', async () => { + const mockFile = { + filepath: 'https://bucket.s3.amazonaws.com/images/user123/file.jpg', + file_id: 'file123', + } as TFile; + + s3Mock.on(HeadObjectCommand).resolvesOnce({}); + const noSuchKeyError = Object.assign(new Error('NoSuchKey'), { name: 'NoSuchKey' }); + s3Mock.on(DeleteObjectCommand).rejects(noSuchKeyError); + + const { deleteFileFromS3 } = await import('../crud'); + await expect(deleteFileFromS3(mockReq, mockFile)).resolves.toBeUndefined(); + expect(deleteRagFile).not.toHaveBeenCalled(); + }); + }); + + describe('uploadFileToS3', () => { + const mockReq = { user: { id: 'user123' } } as ServerRequest; + + it('uploads a file from disk to S3', async () => { + const mockFile = { + path: '/tmp/upload.jpg', + originalname: 'photo.jpg', + } as Express.Multer.File; + + (fs.promises.stat as jest.Mock).mockResolvedValue({ size: 1024 }); + (fs.createReadStream as jest.Mock).mockReturnValue(new Readable()); + + const { uploadFileToS3 } = await import('../crud'); + const result = await uploadFileToS3({ + req: mockReq, + file: mockFile, + file_id: 'file123', + basePath: 'images', + }); + + expect(result).toEqual({ + filepath: expect.stringContaining('signed=true'), + bytes: 1024, + }); + expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload.jpg'); + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + expect(fs.promises.unlink).not.toHaveBeenCalled(); + }); + + it('handles upload errors and cleans up temp file', async () => { + const mockFile = { + path: '/tmp/upload.jpg', + originalname: 'photo.jpg', + } as Express.Multer.File; + + (fs.promises.stat as jest.Mock).mockResolvedValue({ size: 1024 }); + (fs.promises.unlink as jest.Mock).mockResolvedValue(undefined); + (fs.createReadStream as jest.Mock).mockReturnValue(new Readable()); + s3Mock.on(PutObjectCommand).rejects(new Error('Upload failed')); + + const { uploadFileToS3 } = await import('../crud'); + await expect( + uploadFileToS3({ + req: mockReq, + file: mockFile, + file_id: 'file123', + }), + ).rejects.toThrow('Upload failed'); + + expect(logger.error).toHaveBeenCalledWith( + '[uploadFileToS3] Error streaming file to S3:', + expect.any(Error), + ); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/upload.jpg'); + }); + }); + + describe('getS3FileStream', () => { + it('returns a readable stream for a file', async () => { + const { getS3FileStream } = await import('../crud'); + const result = await getS3FileStream( + {} as ServerRequest, + 'https://bucket.s3.amazonaws.com/images/user123/file.pdf', + ); + + expect(result).toBeInstanceOf(Readable); + expect(s3Mock.commandCalls(GetObjectCommand)).toHaveLength(1); + }); + + it('handles errors when retrieving stream', async () => { + s3Mock.on(GetObjectCommand).rejects(new Error('Stream error')); + + const { getS3FileStream } = await import('../crud'); + await expect(getS3FileStream({} as ServerRequest, 'images/user123/file.pdf')).rejects.toThrow( + 'Stream error', + ); + expect(logger.error).toHaveBeenCalled(); + }); + }); + + describe('needsRefresh', () => { + it('returns false for non-signed URLs', async () => { + const { needsRefresh } = await import('../crud'); + const result = needsRefresh('https://example.com/file.png', 3600); + expect(result).toBe(false); + }); + + it('returns true when URL is expired', async () => { + const { needsRefresh } = await import('../crud'); + const pastDate = new Date(Date.now() - 2 * 60 * 60 * 1000); + const dateStr = pastDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + const url = `https://bucket.s3.amazonaws.com/key?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=3600`; + const result = needsRefresh(url, 3600); + expect(result).toBe(true); + }); + + it('returns false when URL is not close to expiration', async () => { + const { needsRefresh } = await import('../crud'); + const futureDate = new Date(Date.now() + 10 * 60 * 1000); + const dateStr = futureDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + const url = `https://bucket.s3.amazonaws.com/key?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=7200`; + const result = needsRefresh(url, 60); + expect(result).toBe(false); + }); + + it('returns true when missing expiration parameters', async () => { + const { needsRefresh } = await import('../crud'); + const url = 'https://bucket.s3.amazonaws.com/key?X-Amz-Signature=abc'; + const result = needsRefresh(url, 3600); + expect(result).toBe(true); + }); + + it('returns true for malformed URLs', async () => { + const { needsRefresh } = await import('../crud'); + const result = needsRefresh('not-a-valid-url', 3600); + expect(result).toBe(true); + }); + }); + + describe('getNewS3URL', () => { + it('generates a new URL from an existing S3 URL', async () => { + const { getNewS3URL } = await import('../crud'); + const result = await getNewS3URL( + 'https://bucket.s3.amazonaws.com/images/user123/file.jpg?signature=old', + ); + + expect(result).toContain('signed=true'); + }); + + it('returns undefined for invalid URLs', async () => { + const { getNewS3URL } = await import('../crud'); + const result = await getNewS3URL('simple-file.txt'); + expect(result).toBeUndefined(); + }); + + it('returns undefined when key has insufficient parts', async () => { + const { getNewS3URL } = await import('../crud'); + // Key with only 2 parts (basePath/userId but no fileName) + const result = await getNewS3URL('https://bucket.s3.amazonaws.com/images/user123'); + expect(result).toBeUndefined(); + }); + }); + + describe('refreshS3FileUrls', () => { + it('refreshes expired URLs for multiple files', async () => { + const { refreshS3FileUrls } = await import('../crud'); + + const pastDate = new Date(Date.now() - 2 * 60 * 60 * 1000); + const dateStr = pastDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + + const files = [ + { + file_id: 'file1', + source: FileSources.s3, + filepath: `https://bucket.s3.amazonaws.com/images/user123/file1.jpg?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, + }, + { + file_id: 'file2', + source: FileSources.s3, + filepath: `https://bucket.s3.amazonaws.com/images/user456/file2.jpg?X-Amz-Signature=def&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, + }, + ]; + + const mockBatchUpdate = jest.fn().mockResolvedValue(undefined); + + const result = await refreshS3FileUrls(files as TFile[], mockBatchUpdate, 60); + + expect(result[0].filepath).toContain('signed=true'); + expect(result[1].filepath).toContain('signed=true'); + expect(mockBatchUpdate).toHaveBeenCalledWith([ + { file_id: 'file1', filepath: expect.stringContaining('signed=true') }, + { file_id: 'file2', filepath: expect.stringContaining('signed=true') }, + ]); + }); + + it('skips non-S3 files', async () => { + const { refreshS3FileUrls } = await import('../crud'); + + const files = [ + { + file_id: 'file1', + source: 'local', + filepath: '/local/path/file.jpg', + }, + ]; + + const mockBatchUpdate = jest.fn(); + + const result = await refreshS3FileUrls(files as TFile[], mockBatchUpdate); + + expect(result).toEqual(files); + expect(mockBatchUpdate).not.toHaveBeenCalled(); + }); + + it('handles empty or invalid input', async () => { + const { refreshS3FileUrls } = await import('../crud'); + const mockBatchUpdate = jest.fn(); + + const result1 = await refreshS3FileUrls(null, mockBatchUpdate); + expect(result1).toEqual([]); + + const result2 = await refreshS3FileUrls(undefined, mockBatchUpdate); + expect(result2).toEqual([]); + + const result3 = await refreshS3FileUrls([], mockBatchUpdate); + expect(result3).toEqual([]); + + expect(mockBatchUpdate).not.toHaveBeenCalled(); + }); + }); + + describe('refreshS3Url', () => { + it('refreshes an expired S3 URL', async () => { + const { refreshS3Url } = await import('../crud'); + + const pastDate = new Date(Date.now() - 2 * 60 * 60 * 1000); + const dateStr = pastDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + + const fileObj: S3FileRef = { + source: FileSources.s3, + filepath: `https://bucket.s3.amazonaws.com/images/user123/file.jpg?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, + }; + + const result = await refreshS3Url(fileObj, 60); + + expect(result).toContain('signed=true'); + }); + + it('returns original URL if not expired', async () => { + const { refreshS3Url } = await import('../crud'); + + const fileObj: S3FileRef = { + source: FileSources.s3, + filepath: 'https://example.com/proxy/file.jpg', + }; + + const result = await refreshS3Url(fileObj, 3600); + + expect(result).toBe(fileObj.filepath); + }); + + it('returns empty string for null input', async () => { + const { refreshS3Url } = await import('../crud'); + const result = await refreshS3Url(null as unknown as S3FileRef); + expect(result).toBe(''); + }); + + it('returns original URL for non-S3 files', async () => { + const { refreshS3Url } = await import('../crud'); + + const fileObj: S3FileRef = { + source: 'local', + filepath: '/local/path/file.jpg', + }; + + const result = await refreshS3Url(fileObj); + + expect(result).toBe(fileObj.filepath); + }); + + it('handles errors and returns original URL', async () => { + (getSignedUrl as jest.Mock).mockRejectedValueOnce(new Error('Refresh failed')); + + const { refreshS3Url } = await import('../crud'); + + const pastDate = new Date(Date.now() - 2 * 60 * 60 * 1000); + const dateStr = pastDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + + const fileObj: S3FileRef = { + source: FileSources.s3, + filepath: `https://bucket.s3.amazonaws.com/images/user123/file.jpg?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=60`, + }; + + const result = await refreshS3Url(fileObj, 60); + + expect(result).toBe(fileObj.filepath); + expect(logger.error).toHaveBeenCalled(); + }); + }); + + describe('extractKeyFromS3Url', () => { + it('extracts key from virtual-hosted-style URL', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url('https://bucket.s3.amazonaws.com/images/user123/file.png'); + expect(key).toBe('images/user123/file.png'); + }); + + it('returns key as-is when not a URL', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url('images/user123/file.png'); + expect(key).toBe('images/user123/file.png'); + }); + + it('throws on empty input', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + expect(() => extractKeyFromS3Url('')).toThrow('Invalid input: URL or key is empty'); + }); + + it('handles URL with query parameters', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url( + 'https://bucket.s3.amazonaws.com/images/user123/file.png?X-Amz-Signature=abc', + ); + expect(key).toBe('images/user123/file.png'); + }); + + it('extracts key from path-style regional endpoint', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url( + 'https://s3.us-west-2.amazonaws.com/test-bucket/dogs/puppy.jpg', + ); + expect(key).toBe('dogs/puppy.jpg'); + }); + + it('extracts key from virtual-hosted regional endpoint', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url( + 'https://test-bucket.s3.us-west-2.amazonaws.com/dogs/puppy.png', + ); + expect(key).toBe('dogs/puppy.png'); + }); + + it('extracts key from legacy s3-region format', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url( + 'https://test-bucket.s3-us-west-2.amazonaws.com/cats/kitten.png', + ); + expect(key).toBe('cats/kitten.png'); + }); + + it('extracts key from legacy global endpoint', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url('https://test-bucket.s3.amazonaws.com/dogs/puppy.png'); + expect(key).toBe('dogs/puppy.png'); + }); + + it('handles key with leading slash by removing it', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url('/images/user123/file.jpg'); + expect(key).toBe('images/user123/file.jpg'); + }); + + it('handles simple key without slashes', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url('simple-file.txt'); + expect(key).toBe('simple-file.txt'); + }); + + it('handles key with only two parts', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url('folder/file.txt'); + expect(key).toBe('folder/file.txt'); + }); + + it('handles URLs with encoded characters', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url( + 'https://bucket.s3.amazonaws.com/test-bucket/images/user123/my%20file%20name.jpg', + ); + expect(key).toBe('images/user123/my%20file%20name.jpg'); + }); + + it('handles deep nested paths', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url( + 'https://bucket.s3.amazonaws.com/test-bucket/a/b/c/d/e/f/file.jpg', + ); + expect(key).toBe('a/b/c/d/e/f/file.jpg'); + }); + + it('returns empty string for URL with only bucket (no key)', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url('https://s3.us-west-2.amazonaws.com/my-bucket'); + expect(key).toBe(''); + expect(logger.warn).toHaveBeenCalled(); + }); + + it('handles malformed URL and returns input', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const malformedUrl = 'https://invalid url with spaces.com/key'; + const result = extractKeyFromS3Url(malformedUrl); + + expect(logger.error).toHaveBeenCalled(); + expect(result).toBe(malformedUrl); + }); + + it('strips bucket from custom endpoint URLs (MinIO, R2)', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = extractKeyFromS3Url( + 'https://minio.example.com/test-bucket/images/user123/file.jpg', + ); + expect(key).toBe('images/user123/file.jpg'); + }); + }); + + describe('needsRefresh with S3_REFRESH_EXPIRY_MS set', () => { + beforeEach(() => { + process.env.S3_REFRESH_EXPIRY_MS = '60000'; // 1 minute + jest.resetModules(); + }); + + afterEach(() => { + delete process.env.S3_REFRESH_EXPIRY_MS; + }); + + it('returns true when URL age exceeds S3_REFRESH_EXPIRY_MS', async () => { + const { needsRefresh } = await import('../crud'); + // URL created 2 minutes ago + const oldDate = new Date(Date.now() - 2 * 60 * 1000); + const dateStr = oldDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + const url = `https://bucket.s3.amazonaws.com/key?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=3600`; + + const result = needsRefresh(url, 60); + expect(result).toBe(true); + }); + + it('returns false when URL age is under S3_REFRESH_EXPIRY_MS', async () => { + const { needsRefresh } = await import('../crud'); + // URL created 30 seconds ago + const recentDate = new Date(Date.now() - 30 * 1000); + const dateStr = recentDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + const url = `https://bucket.s3.amazonaws.com/key?X-Amz-Signature=abc&X-Amz-Date=${dateStr}&X-Amz-Expires=3600`; + + const result = needsRefresh(url, 60); + expect(result).toBe(false); + }); + }); +}); diff --git a/packages/api/src/storage/s3/__tests__/images.test.ts b/packages/api/src/storage/s3/__tests__/images.test.ts new file mode 100644 index 0000000000..065c73cebd --- /dev/null +++ b/packages/api/src/storage/s3/__tests__/images.test.ts @@ -0,0 +1,182 @@ +import fs from 'fs'; +import type { S3ImageServiceDeps } from '~/storage/s3/images'; +import type { ServerRequest } from '~/types'; +import { S3ImageService } from '~/storage/s3/images'; +import { saveBufferToS3 } from '~/storage/s3/crud'; + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + promises: { + readFile: jest.fn(), + unlink: jest.fn().mockResolvedValue(undefined), + }, +})); + +jest.mock('../crud', () => ({ + saveBufferToS3: jest + .fn() + .mockResolvedValue('https://bucket.s3.amazonaws.com/avatar.png?signed=true'), +})); + +const mockSaveBufferToS3 = jest.mocked(saveBufferToS3); + +jest.mock('sharp', () => { + return jest.fn(() => ({ + metadata: jest.fn().mockResolvedValue({ format: 'png', width: 100, height: 100 }), + toFormat: jest.fn().mockReturnThis(), + toBuffer: jest.fn().mockResolvedValue(Buffer.from('processed')), + })); +}); + +describe('S3ImageService', () => { + let service: S3ImageService; + let mockDeps: S3ImageServiceDeps; + + beforeEach(() => { + jest.clearAllMocks(); + + mockDeps = { + resizeImageBuffer: jest.fn().mockResolvedValue({ + buffer: Buffer.from('resized'), + width: 100, + height: 100, + }), + updateUser: jest.fn().mockResolvedValue(undefined), + updateFile: jest.fn().mockResolvedValue(undefined), + }; + + service = new S3ImageService(mockDeps); + }); + + describe('processAvatar', () => { + it('uploads avatar and returns URL', async () => { + const result = await service.processAvatar({ + buffer: Buffer.from('test'), + userId: 'user123', + manual: 'false', + }); + + expect(result).toContain('signed=true'); + }); + + it('updates user avatar when manual is true', async () => { + await service.processAvatar({ + buffer: Buffer.from('test'), + userId: 'user123', + manual: 'true', + }); + + expect(mockDeps.updateUser).toHaveBeenCalledWith( + 'user123', + expect.objectContaining({ avatar: expect.any(String) }), + ); + }); + + it('does not update user when agentId is provided', async () => { + await service.processAvatar({ + buffer: Buffer.from('test'), + userId: 'user123', + manual: 'true', + agentId: 'agent456', + }); + + expect(mockDeps.updateUser).not.toHaveBeenCalled(); + }); + + it('generates agent avatar filename when agentId provided', async () => { + await service.processAvatar({ + buffer: Buffer.from('test'), + userId: 'user123', + manual: 'false', + agentId: 'agent456', + }); + + expect(mockSaveBufferToS3).toHaveBeenCalledWith( + expect.objectContaining({ + fileName: expect.stringContaining('agent-agent456-avatar-'), + }), + ); + }); + }); + + describe('prepareImageURL', () => { + it('returns tuple with resolved promise and filepath', async () => { + const file = { file_id: 'file123', filepath: 'https://example.com/file.png' }; + const result = await service.prepareImageURL(file); + + expect(Array.isArray(result)).toBe(true); + expect(result[1]).toBe('https://example.com/file.png'); + }); + + it('calls updateFile with file_id', async () => { + const file = { file_id: 'file123', filepath: 'https://example.com/file.png' }; + await service.prepareImageURL(file); + + expect(mockDeps.updateFile).toHaveBeenCalledWith({ file_id: 'file123' }); + }); + }); + + describe('constructor', () => { + it('requires dependencies to be passed', () => { + const newService = new S3ImageService(mockDeps); + expect(newService).toBeInstanceOf(S3ImageService); + }); + }); + + describe('uploadImageToS3', () => { + const mockReq = { + user: { id: 'user123' }, + config: { imageOutputType: 'webp' }, + } as unknown as ServerRequest; + + it('deletes temp file on early failure (readFile throws)', async () => { + (fs.promises.readFile as jest.Mock).mockRejectedValueOnce( + new Error('ENOENT: no such file or directory'), + ); + (fs.promises.unlink as jest.Mock).mockResolvedValueOnce(undefined); + + await expect( + service.uploadImageToS3({ + req: mockReq, + file: { path: '/tmp/input.jpg' } as Express.Multer.File, + file_id: 'file123', + endpoint: 'openai', + }), + ).rejects.toThrow('ENOENT: no such file or directory'); + + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/input.jpg'); + }); + + it('deletes temp file on resize failure (resizeImageBuffer throws)', async () => { + (fs.promises.readFile as jest.Mock).mockResolvedValueOnce(Buffer.from('raw')); + (mockDeps.resizeImageBuffer as jest.Mock).mockRejectedValueOnce(new Error('Resize failed')); + (fs.promises.unlink as jest.Mock).mockResolvedValueOnce(undefined); + + await expect( + service.uploadImageToS3({ + req: mockReq, + file: { path: '/tmp/input.jpg' } as Express.Multer.File, + file_id: 'file123', + endpoint: 'openai', + }), + ).rejects.toThrow('Resize failed'); + + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/input.jpg'); + }); + + it('deletes temp file on success', async () => { + (fs.promises.readFile as jest.Mock).mockResolvedValueOnce(Buffer.from('raw')); + (fs.promises.unlink as jest.Mock).mockResolvedValueOnce(undefined); + + const result = await service.uploadImageToS3({ + req: mockReq, + file: { path: '/tmp/input.webp' } as Express.Multer.File, + file_id: 'file123', + endpoint: 'openai', + }); + + expect(result.filepath).toContain('signed=true'); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/input.webp'); + }); + }); +}); diff --git a/packages/api/src/storage/s3/__tests__/s3.integration.spec.ts b/packages/api/src/storage/s3/__tests__/s3.integration.spec.ts new file mode 100644 index 0000000000..de80e7409b --- /dev/null +++ b/packages/api/src/storage/s3/__tests__/s3.integration.spec.ts @@ -0,0 +1,529 @@ +/** + * S3 Integration Tests + * + * These tests run against a REAL S3 bucket. They are skipped when AWS_TEST_BUCKET_NAME is not set. + * + * Run with: + * AWS_TEST_BUCKET_NAME=my-test-bucket npx jest s3.s3_integration + * + * Required env vars: + * - AWS_TEST_BUCKET_NAME: Dedicated test bucket (gates test execution) + * - AWS_REGION: Defaults to 'us-east-1' + * - AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY => to avoid error: A dynamic import callback was invoked without -experimental-vm-modules — the AWS SDK credential provider + */ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { Readable } from 'stream'; +import { ListObjectsV2Command, DeleteObjectsCommand } from '@aws-sdk/client-s3'; +import type { S3Client } from '@aws-sdk/client-s3'; +import type { ServerRequest } from '~/types'; + +const MINIMAL_PNG = Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, + 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0x3f, + 0x00, 0x05, 0xfe, 0x02, 0xfe, 0xdc, 0xcc, 0x59, 0xe7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, + 0x44, 0xae, 0x42, 0x60, 0x82, +]); + +const TEST_BUCKET = process.env.AWS_TEST_BUCKET_NAME; +const TEST_USER_ID = 'test-user-123'; +const TEST_RUN_ID = `integration-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +const TEST_BASE_PATH = TEST_RUN_ID; + +async function deleteAllWithPrefix(s3: S3Client, bucket: string, prefix: string): Promise { + let continuationToken: string | undefined; + + do { + const listCommand = new ListObjectsV2Command({ + Bucket: bucket, + Prefix: prefix, + ContinuationToken: continuationToken, + }); + const response = await s3.send(listCommand); + + if (response.Contents?.length) { + const deleteCommand = new DeleteObjectsCommand({ + Bucket: bucket, + Delete: { + Objects: response.Contents.filter( + (obj): obj is typeof obj & { Key: string } => obj.Key !== undefined, + ).map((obj) => ({ Key: obj.Key })), + }, + }); + await s3.send(deleteCommand); + } + + continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; + } while (continuationToken); +} + +describe('S3 Integration Tests', () => { + if (!TEST_BUCKET) { + // eslint-disable-next-line jest/expect-expect + it.skip('Skipped: AWS_TEST_BUCKET_NAME not configured', () => {}); + return; + } + + let originalEnv: NodeJS.ProcessEnv; + let tempDir: string; + let s3Client: S3Client | null = null; + + beforeAll(async () => { + originalEnv = { ...process.env }; + + // Use dedicated test bucket + process.env.AWS_BUCKET_NAME = TEST_BUCKET; + process.env.AWS_REGION = process.env.AWS_REGION || 'us-east-1'; + + // Reset modules so the next import picks up the updated env vars. + // s3Client is retained as a plain instance — it remains valid even though + // beforeEach/afterEach call resetModules() for per-test isolation. + jest.resetModules(); + const { initializeS3 } = await import('~/cdn/s3'); + s3Client = initializeS3(); + }); + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 's3-integration-')); + jest.resetModules(); + }); + + afterEach(async () => { + if (tempDir && fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + jest.resetModules(); + }); + + afterAll(async () => { + // Clean up all test files from this run + if (s3Client && TEST_BUCKET) { + await deleteAllWithPrefix(s3Client, TEST_BUCKET, TEST_RUN_ID); + } + process.env = originalEnv; + jest.resetModules(); + }); + + describe('getS3Key', () => { + it('constructs key from basePath, userId, and fileName', async () => { + const { getS3Key } = await import('../crud'); + const key = getS3Key(TEST_BASE_PATH, TEST_USER_ID, 'test-file.txt'); + expect(key).toBe(`${TEST_BASE_PATH}/${TEST_USER_ID}/test-file.txt`); + }); + + it('handles nested file names', async () => { + const { getS3Key } = await import('../crud'); + const key = getS3Key(TEST_BASE_PATH, TEST_USER_ID, 'folder/nested/file.pdf'); + expect(key).toBe(`${TEST_BASE_PATH}/${TEST_USER_ID}/folder/nested/file.pdf`); + }); + }); + + describe('saveBufferToS3 and getS3URL', () => { + it('uploads buffer and returns signed URL', async () => { + const { saveBufferToS3 } = await import('../crud'); + const testContent = 'Hello, S3!'; + const buffer = Buffer.from(testContent); + const fileName = `test-${Date.now()}.txt`; + + const downloadURL = await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + expect(downloadURL).toBeDefined(); + expect(downloadURL).toContain('X-Amz-Signature'); + expect(downloadURL).toContain(fileName); + }); + + it('can get signed URL for existing file', async () => { + const { saveBufferToS3, getS3URL } = await import('../crud'); + const buffer = Buffer.from('test content for URL'); + const fileName = `url-test-${Date.now()}.txt`; + + await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const signedUrl = await getS3URL({ + userId: TEST_USER_ID, + fileName, + basePath: TEST_BASE_PATH, + }); + + expect(signedUrl).toBeDefined(); + expect(signedUrl).toContain('X-Amz-Signature'); + }); + + it('can get signed URL with custom filename and content type', async () => { + const { saveBufferToS3, getS3URL } = await import('../crud'); + const buffer = Buffer.from('custom headers test'); + const fileName = `headers-test-${Date.now()}.txt`; + + await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const signedUrl = await getS3URL({ + userId: TEST_USER_ID, + fileName, + basePath: TEST_BASE_PATH, + customFilename: 'download.txt', + contentType: 'text/plain', + }); + + expect(signedUrl).toContain('response-content-disposition'); + expect(signedUrl).toContain('response-content-type'); + }); + }); + + describe('saveURLToS3', () => { + it('fetches URL content and uploads to S3', async () => { + const { saveURLToS3 } = await import('../crud'); + const fileName = `url-upload-${Date.now()}.json`; + + const downloadURL = await saveURLToS3({ + userId: TEST_USER_ID, + URL: 'https://raw.githubusercontent.com/danny-avila/LibreChat/main/package.json', + fileName, + basePath: TEST_BASE_PATH, + }); + + expect(downloadURL).toBeDefined(); + expect(downloadURL).toContain('X-Amz-Signature'); + }); + }); + + describe('extractKeyFromS3Url', () => { + it('extracts key from signed URL', async () => { + const { saveBufferToS3, extractKeyFromS3Url } = await import('../crud'); + const buffer = Buffer.from('extract key test'); + const fileName = `extract-key-${Date.now()}.txt`; + + const signedUrl = await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const extractedKey = extractKeyFromS3Url(signedUrl); + expect(extractedKey).toBe(`${TEST_BASE_PATH}/${TEST_USER_ID}/${fileName}`); + }); + + it('returns key as-is when not a URL', async () => { + const { extractKeyFromS3Url } = await import('../crud'); + const key = `${TEST_BASE_PATH}/${TEST_USER_ID}/file.txt`; + expect(extractKeyFromS3Url(key)).toBe(key); + }); + }); + + describe('uploadFileToS3', () => { + it('uploads file and returns filepath with bytes', async () => { + const { uploadFileToS3 } = await import('../crud'); + const testContent = 'File upload test content'; + const testFilePath = path.join(tempDir, 'upload-test.txt'); + fs.writeFileSync(testFilePath, testContent); + + const mockReq = { + user: { id: TEST_USER_ID }, + } as ServerRequest; + + const mockFile = { + path: testFilePath, + originalname: 'upload-test.txt', + fieldname: 'file', + encoding: '7bit', + mimetype: 'text/plain', + size: Buffer.byteLength(testContent), + stream: fs.createReadStream(testFilePath), + destination: tempDir, + filename: 'upload-test.txt', + buffer: Buffer.from(testContent), + } as Express.Multer.File; + + const fileId = `file-${Date.now()}`; + + const result = await uploadFileToS3({ + req: mockReq, + file: mockFile, + file_id: fileId, + basePath: TEST_BASE_PATH, + }); + + expect(result.filepath).toBeDefined(); + expect(result.filepath).toContain('X-Amz-Signature'); + expect(result.bytes).toBe(Buffer.byteLength(testContent)); + }); + + it('throws error when user is not authenticated', async () => { + const { uploadFileToS3 } = await import('../crud'); + const mockReq = {} as ServerRequest; + const mockFile = { + path: '/fake/path.txt', + originalname: 'test.txt', + } as Express.Multer.File; + + await expect( + uploadFileToS3({ + req: mockReq, + file: mockFile, + file_id: 'test-id', + basePath: TEST_BASE_PATH, + }), + ).rejects.toThrow('User not authenticated'); + }); + }); + + describe('getS3FileStream', () => { + it('returns readable stream for existing file', async () => { + const { saveBufferToS3, getS3FileStream } = await import('../crud'); + const testContent = 'Stream test content'; + const buffer = Buffer.from(testContent); + const fileName = `stream-test-${Date.now()}.txt`; + + const signedUrl = await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const mockReq = { + user: { id: TEST_USER_ID }, + } as ServerRequest; + + const stream = await getS3FileStream(mockReq, signedUrl); + + expect(stream).toBeInstanceOf(Readable); + + const chunks: Uint8Array[] = []; + for await (const chunk of stream) { + chunks.push(chunk as Uint8Array); + } + const downloadedContent = Buffer.concat(chunks).toString(); + expect(downloadedContent).toBe(testContent); + }); + }); + + describe('needsRefresh', () => { + it('returns false for non-signed URLs', async () => { + const { needsRefresh } = await import('../crud'); + expect(needsRefresh('https://example.com/file.png', 3600)).toBe(false); + }); + + it('returns true for expired signed URLs', async () => { + const { saveBufferToS3, needsRefresh } = await import('../crud'); + const buffer = Buffer.from('refresh test'); + const fileName = `refresh-test-${Date.now()}.txt`; + + const signedUrl = await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const result = needsRefresh(signedUrl, 999999); + expect(result).toBe(true); + }); + + it('returns false for fresh signed URLs', async () => { + const { saveBufferToS3, needsRefresh } = await import('../crud'); + const buffer = Buffer.from('fresh test'); + const fileName = `fresh-test-${Date.now()}.txt`; + + const signedUrl = await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const result = needsRefresh(signedUrl, 60); + expect(result).toBe(false); + }); + }); + + describe('getNewS3URL', () => { + it('generates signed URL from existing URL', async () => { + const { saveBufferToS3, getNewS3URL } = await import('../crud'); + const buffer = Buffer.from('new url test'); + const fileName = `new-url-${Date.now()}.txt`; + + const originalUrl = await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const newUrl = await getNewS3URL(originalUrl); + + expect(newUrl).toBeDefined(); + expect(newUrl).toContain('X-Amz-Signature'); + expect(newUrl).toContain(fileName); + }); + }); + + describe('refreshS3Url', () => { + it('returns original URL for non-S3 source', async () => { + const { refreshS3Url } = await import('../crud'); + const fileObj = { + filepath: 'https://example.com/file.png', + source: 'local', + }; + + const result = await refreshS3Url(fileObj, 3600); + expect(result).toBe(fileObj.filepath); + }); + + it('refreshes URL for S3 source when needed', async () => { + const { saveBufferToS3, refreshS3Url } = await import('../crud'); + const buffer = Buffer.from('s3 refresh test'); + const fileName = `s3-refresh-${Date.now()}.txt`; + + const originalUrl = await saveBufferToS3({ + userId: TEST_USER_ID, + buffer, + fileName, + basePath: TEST_BASE_PATH, + }); + + const fileObj = { + filepath: originalUrl, + source: 's3', + }; + + const newUrl = await refreshS3Url(fileObj, 999999); + + expect(newUrl).toBeDefined(); + expect(newUrl).toContain('X-Amz-Signature'); + }); + }); + + describe('S3ImageService', () => { + it('uploads avatar and returns URL', async () => { + const { S3ImageService } = await import('../images'); + + const mockDeps = { + resizeImageBuffer: jest.fn().mockImplementation(async (buffer: Buffer) => ({ + buffer, + width: 100, + height: 100, + })), + updateUser: jest.fn().mockResolvedValue(undefined), + updateFile: jest.fn().mockResolvedValue(undefined), + }; + + const imageService = new S3ImageService(mockDeps); + + const pngBuffer = MINIMAL_PNG; + + const result = await imageService.processAvatar({ + buffer: pngBuffer, + userId: TEST_USER_ID, + manual: 'false', + basePath: TEST_BASE_PATH, + }); + + expect(result).toBeDefined(); + expect(result).toContain('X-Amz-Signature'); + expect(result).toContain('avatar'); + }); + + it('updates user when manual is true', async () => { + const { S3ImageService } = await import('../images'); + + const mockDeps = { + resizeImageBuffer: jest.fn().mockImplementation(async (buffer: Buffer) => ({ + buffer, + width: 100, + height: 100, + })), + updateUser: jest.fn().mockResolvedValue(undefined), + updateFile: jest.fn().mockResolvedValue(undefined), + }; + + const imageService = new S3ImageService(mockDeps); + + const pngBuffer = MINIMAL_PNG; + + await imageService.processAvatar({ + buffer: pngBuffer, + userId: TEST_USER_ID, + manual: 'true', + basePath: TEST_BASE_PATH, + }); + + expect(mockDeps.updateUser).toHaveBeenCalledWith( + TEST_USER_ID, + expect.objectContaining({ avatar: expect.any(String) }), + ); + }); + + it('does not update user when agentId is provided', async () => { + const { S3ImageService } = await import('../images'); + + const mockDeps = { + resizeImageBuffer: jest.fn().mockImplementation(async (buffer: Buffer) => ({ + buffer, + width: 100, + height: 100, + })), + updateUser: jest.fn().mockResolvedValue(undefined), + updateFile: jest.fn().mockResolvedValue(undefined), + }; + + const imageService = new S3ImageService(mockDeps); + + const pngBuffer = MINIMAL_PNG; + + await imageService.processAvatar({ + buffer: pngBuffer, + userId: TEST_USER_ID, + manual: 'true', + agentId: 'agent-123', + basePath: TEST_BASE_PATH, + }); + + expect(mockDeps.updateUser).not.toHaveBeenCalled(); + }); + + it('returns tuple with resolved promise and filepath in prepareImageURL', async () => { + const { S3ImageService } = await import('../images'); + + const mockDeps = { + resizeImageBuffer: jest.fn().mockImplementation(async (buffer: Buffer) => ({ + buffer, + width: 100, + height: 100, + })), + updateUser: jest.fn().mockResolvedValue(undefined), + updateFile: jest.fn().mockResolvedValue(undefined), + }; + + const imageService = new S3ImageService(mockDeps); + + const testFile = { + file_id: 'file-123', + filepath: 'https://example.com/file.png', + }; + + const result = await imageService.prepareImageURL(testFile); + + expect(Array.isArray(result)).toBe(true); + expect(result[1]).toBe(testFile.filepath); + expect(mockDeps.updateFile).toHaveBeenCalledWith({ file_id: 'file-123' }); + }); + }); +}); diff --git a/packages/api/src/storage/s3/crud.ts b/packages/api/src/storage/s3/crud.ts new file mode 100644 index 0000000000..1143a7ed7f --- /dev/null +++ b/packages/api/src/storage/s3/crud.ts @@ -0,0 +1,460 @@ +import fs from 'fs'; +import { Readable } from 'stream'; +import { logger } from '@librechat/data-schemas'; +import { FileSources } from 'librechat-data-provider'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { + PutObjectCommand, + GetObjectCommand, + HeadObjectCommand, + DeleteObjectCommand, +} from '@aws-sdk/client-s3'; +import type { GetObjectCommandInput } from '@aws-sdk/client-s3'; +import type { TFile } from 'librechat-data-provider'; +import type { ServerRequest } from '~/types'; +import type { + UploadFileParams, + SaveBufferParams, + BatchUpdateFn, + SaveURLParams, + GetURLParams, + UploadResult, + S3FileRef, +} from '~/storage/types'; +import { initializeS3 } from '~/cdn/s3'; +import { deleteRagFile } from '~/files'; +import { s3Config } from './s3Config'; + +const { + AWS_BUCKET_NAME: bucketName, + AWS_ENDPOINT_URL: endpoint, + AWS_FORCE_PATH_STYLE: forcePathStyle, + S3_URL_EXPIRY_SECONDS: s3UrlExpirySeconds, + S3_REFRESH_EXPIRY_MS: s3RefreshExpiryMs, + DEFAULT_BASE_PATH: defaultBasePath, +} = s3Config; + +export const getS3Key = (basePath: string, userId: string, fileName: string): string => { + if (basePath.includes('/')) { + throw new Error(`[getS3Key] basePath must not contain slashes: "${basePath}"`); + } + return `${basePath}/${userId}/${fileName}`; +}; + +export async function getS3URL({ + userId, + fileName, + basePath = defaultBasePath, + customFilename = null, + contentType = null, +}: GetURLParams): Promise { + const key = getS3Key(basePath, userId, fileName); + const params: GetObjectCommandInput = { Bucket: bucketName, Key: key }; + + if (customFilename) { + const safeFilename = customFilename.replace(/["\r\n]/g, ''); + params.ResponseContentDisposition = `attachment; filename="${safeFilename}"`; + } + if (contentType) { + params.ResponseContentType = contentType; + } + + try { + const s3 = initializeS3(); + if (!s3) { + throw new Error('[getS3URL] S3 not initialized'); + } + + return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: s3UrlExpirySeconds }); + } catch (error) { + logger.error('[getS3URL] Error getting signed URL from S3:', (error as Error).message); + throw error; + } +} + +export async function saveBufferToS3({ + userId, + buffer, + fileName, + basePath = defaultBasePath, +}: SaveBufferParams): Promise { + const key = getS3Key(basePath, userId, fileName); + const params = { Bucket: bucketName, Key: key, Body: buffer }; + + try { + const s3 = initializeS3(); + if (!s3) { + throw new Error('[saveBufferToS3] S3 not initialized'); + } + + await s3.send(new PutObjectCommand(params)); + return await getS3URL({ userId, fileName, basePath }); + } catch (error) { + logger.error('[saveBufferToS3] Error uploading buffer to S3:', (error as Error).message); + throw error; + } +} + +export async function saveURLToS3({ + userId, + URL, + fileName, + basePath = defaultBasePath, +}: SaveURLParams): Promise { + try { + const response = await fetch(URL); + if (!response.ok) { + throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`); + } + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + return await saveBufferToS3({ userId, buffer, fileName, basePath }); + } catch (error) { + logger.error('[saveURLToS3] Error uploading file from URL to S3:', (error as Error).message); + throw error; + } +} + +export function extractKeyFromS3Url(fileUrlOrKey: string): string { + if (!fileUrlOrKey) { + throw new Error('Invalid input: URL or key is empty'); + } + + try { + const url = new URL(fileUrlOrKey); + const hostname = url.hostname; + const pathname = url.pathname.substring(1); + + if (endpoint && forcePathStyle) { + const endpointUrl = new URL(endpoint); + const startPos = + endpointUrl.pathname.length + + (endpointUrl.pathname.endsWith('/') ? 0 : 1) + + bucketName.length + + 1; + const key = url.pathname.substring(startPos); + if (!key) { + logger.warn( + `[extractKeyFromS3Url] Extracted key is empty for endpoint path-style URL: ${fileUrlOrKey}`, + ); + } else { + logger.debug(`[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`); + } + return key; + } + + if ( + hostname === 's3.amazonaws.com' || + hostname.match(/^s3[-.][a-z0-9-]+\.amazonaws\.com$/) || + (bucketName && pathname.startsWith(`${bucketName}/`)) + ) { + const firstSlashIndex = pathname.indexOf('/'); + if (firstSlashIndex > 0) { + const key = pathname.substring(firstSlashIndex + 1); + if (key === '') { + logger.warn( + `[extractKeyFromS3Url] Extracted key is empty after removing bucket name from URL: ${fileUrlOrKey}`, + ); + } else { + logger.debug( + `[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`, + ); + } + return key; + } + logger.warn( + `[extractKeyFromS3Url] Unable to extract key from path-style URL: ${fileUrlOrKey}`, + ); + return ''; + } + + logger.debug(`[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${pathname}`); + return pathname; + } catch (error) { + if (fileUrlOrKey.startsWith('http://') || fileUrlOrKey.startsWith('https://')) { + logger.error( + `[extractKeyFromS3Url] Error parsing URL: ${fileUrlOrKey}, Error: ${(error as Error).message}`, + ); + } else { + logger.debug(`[extractKeyFromS3Url] Non-URL input, using fallback: ${fileUrlOrKey}`); + } + + const parts = fileUrlOrKey.split('/'); + if (parts.length >= 3 && !fileUrlOrKey.startsWith('http') && !fileUrlOrKey.startsWith('/')) { + return fileUrlOrKey; + } + + const key = fileUrlOrKey.startsWith('/') ? fileUrlOrKey.substring(1) : fileUrlOrKey; + logger.debug( + `[extractKeyFromS3Url] FALLBACK. fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`, + ); + return key; + } +} + +export async function deleteFileFromS3(req: ServerRequest, file: TFile): Promise { + if (!req.user) { + throw new Error('[deleteFileFromS3] User not authenticated'); + } + + const userId = req.user.id; + const key = extractKeyFromS3Url(file.filepath); + + const keyParts = key.split('/'); + if (keyParts.length < 2 || keyParts[1] !== userId) { + const message = `[deleteFileFromS3] User ID mismatch: ${userId} vs ${key}`; + logger.error(message); + throw new Error(message); + } + + const s3 = initializeS3(); + if (!s3) { + throw new Error('[deleteFileFromS3] S3 not initialized'); + } + + const params = { Bucket: bucketName, Key: key }; + + try { + try { + const headCommand = new HeadObjectCommand(params); + await s3.send(headCommand); + logger.debug('[deleteFileFromS3] File exists, proceeding with deletion'); + } catch (headErr) { + if ((headErr as { name?: string }).name === 'NotFound') { + logger.warn(`[deleteFileFromS3] File does not exist: ${key}`); + await deleteRagFile({ userId, file }); + return; + } + throw headErr; + } + + await s3.send(new DeleteObjectCommand(params)); + await deleteRagFile({ userId, file }); + logger.debug('[deleteFileFromS3] S3 File deletion completed'); + } catch (error) { + logger.error(`[deleteFileFromS3] Error deleting file from S3: ${(error as Error).message}`); + logger.error((error as Error).stack); + + if ((error as { name?: string }).name === 'NoSuchKey') { + return; + } + throw error; + } +} + +export async function uploadFileToS3({ + req, + file, + file_id, + basePath = defaultBasePath, +}: UploadFileParams): Promise { + if (!req.user) { + throw new Error('[uploadFileToS3] User not authenticated'); + } + + try { + const inputFilePath = file.path; + const userId = req.user.id; + const fileName = `${file_id}__${file.originalname}`; + const key = getS3Key(basePath, userId, fileName); + + const stats = await fs.promises.stat(inputFilePath); + const bytes = stats.size; + const fileStream = fs.createReadStream(inputFilePath); + + const s3 = initializeS3(); + if (!s3) { + throw new Error('[uploadFileToS3] S3 not initialized'); + } + + const uploadParams = { + Bucket: bucketName, + Key: key, + Body: fileStream, + }; + + await s3.send(new PutObjectCommand(uploadParams)); + const fileURL = await getS3URL({ userId, fileName, basePath }); + // NOTE: temp file is intentionally NOT deleted on the success path. + // The caller (processAgentFileUpload) reads file.path after this returns + // to stream the file to the RAG vector embedding service (POST /embed). + // Temp file lifecycle on success is the caller's responsibility. + return { filepath: fileURL, bytes }; + } catch (error) { + logger.error('[uploadFileToS3] Error streaming file to S3:', error); + if (file?.path) { + await fs.promises + .unlink(file.path) + .catch((e: unknown) => + logger.error('[uploadFileToS3] Failed to delete temp file:', (e as Error).message), + ); + } + throw error; + } +} + +export async function getS3FileStream(_req: ServerRequest, filePath: string): Promise { + try { + const Key = extractKeyFromS3Url(filePath); + const params = { Bucket: bucketName, Key }; + + const s3 = initializeS3(); + if (!s3) { + throw new Error('[getS3FileStream] S3 not initialized'); + } + + const data = await s3.send(new GetObjectCommand(params)); + if (!data.Body) { + throw new Error(`[getS3FileStream] S3 response body is empty for key: ${Key}`); + } + return data.Body as Readable; + } catch (error) { + logger.error('[getS3FileStream] Error retrieving S3 file stream:', error); + throw error; + } +} + +export function needsRefresh(signedUrl: string, bufferSeconds: number): boolean { + try { + const url = new URL(signedUrl); + + if (!url.searchParams.has('X-Amz-Signature')) { + return false; + } + + const expiresParam = url.searchParams.get('X-Amz-Expires'); + const dateParam = url.searchParams.get('X-Amz-Date'); + + if (!expiresParam || !dateParam) { + return true; + } + + const year = dateParam.substring(0, 4); + const month = dateParam.substring(4, 6); + const day = dateParam.substring(6, 8); + const hour = dateParam.substring(9, 11); + const minute = dateParam.substring(11, 13); + const second = dateParam.substring(13, 15); + + const dateObj = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`); + const now = new Date(); + + if (s3RefreshExpiryMs !== null) { + const urlAge = now.getTime() - dateObj.getTime(); + return urlAge >= s3RefreshExpiryMs; + } + + const expiresAtDate = new Date(dateObj.getTime() + parseInt(expiresParam) * 1000); + const bufferTime = new Date(now.getTime() + bufferSeconds * 1000); + return expiresAtDate <= bufferTime; + } catch (error) { + logger.error('Error checking URL expiration:', error); + return true; + } +} + +export async function getNewS3URL(currentURL: string): Promise { + try { + const s3Key = extractKeyFromS3Url(currentURL); + if (!s3Key) { + return; + } + + const keyParts = s3Key.split('/'); + if (keyParts.length < 3) { + return; + } + + const basePath = keyParts[0]; + const userId = keyParts[1]; + const fileName = keyParts.slice(2).join('/'); + + return getS3URL({ userId, fileName, basePath }); + } catch (error) { + logger.error('Error getting new S3 URL:', error); + } +} + +export async function refreshS3FileUrls( + files: TFile[] | null | undefined, + batchUpdateFiles: BatchUpdateFn, + bufferSeconds = 3600, +): Promise { + if (!files || !Array.isArray(files) || files.length === 0) { + return []; + } + + const filesToUpdate: Array<{ file_id: string; filepath: string }> = []; + const updatedFiles = [...files]; + + for (let i = 0; i < updatedFiles.length; i++) { + const file = updatedFiles[i]; + if (!file?.file_id) { + continue; + } + if (file.source !== FileSources.s3) { + continue; + } + if (!file.filepath) { + continue; + } + if (!needsRefresh(file.filepath, bufferSeconds)) { + continue; + } + + try { + const newURL = await getNewS3URL(file.filepath); + if (!newURL) { + continue; + } + filesToUpdate.push({ + file_id: file.file_id, + filepath: newURL, + }); + updatedFiles[i] = { ...file, filepath: newURL }; + } catch (error) { + logger.error(`Error refreshing S3 URL for file ${file.file_id}:`, error); + } + } + + if (filesToUpdate.length > 0) { + await batchUpdateFiles(filesToUpdate); + } + + return updatedFiles; +} + +export async function refreshS3Url(fileObj: S3FileRef, bufferSeconds = 3600): Promise { + if (!fileObj || fileObj.source !== FileSources.s3 || !fileObj.filepath) { + return fileObj?.filepath || ''; + } + + if (!needsRefresh(fileObj.filepath, bufferSeconds)) { + return fileObj.filepath; + } + + try { + const s3Key = extractKeyFromS3Url(fileObj.filepath); + if (!s3Key) { + logger.warn(`Unable to extract S3 key from URL: ${fileObj.filepath}`); + return fileObj.filepath; + } + + const keyParts = s3Key.split('/'); + if (keyParts.length < 3) { + logger.warn(`Invalid S3 key format: ${s3Key}`); + return fileObj.filepath; + } + + const basePath = keyParts[0]; + const userId = keyParts[1]; + const fileName = keyParts.slice(2).join('/'); + + const newUrl = await getS3URL({ userId, fileName, basePath }); + logger.debug(`Refreshed S3 URL for key: ${s3Key}`); + return newUrl; + } catch (error) { + logger.error(`Error refreshing S3 URL: ${(error as Error).message}`); + return fileObj.filepath; + } +} diff --git a/packages/api/src/storage/s3/images.ts b/packages/api/src/storage/s3/images.ts new file mode 100644 index 0000000000..b9d7322359 --- /dev/null +++ b/packages/api/src/storage/s3/images.ts @@ -0,0 +1,141 @@ +import fs from 'fs'; +import path from 'path'; +import sharp from 'sharp'; +import { logger } from '@librechat/data-schemas'; +import type { IUser } from '@librechat/data-schemas'; +import type { TFile } from 'librechat-data-provider'; +import type { FormatEnum } from 'sharp'; +import type { UploadImageParams, ImageUploadResult, ProcessAvatarParams } from '~/storage/types'; +import { saveBufferToS3 } from './crud'; +import { s3Config } from './s3Config'; + +const { DEFAULT_BASE_PATH: defaultBasePath } = s3Config; + +export interface S3ImageServiceDeps { + resizeImageBuffer: ( + buffer: Buffer, + resolution: string, + endpoint: string, + ) => Promise<{ buffer: Buffer; width: number; height: number }>; + updateUser: (userId: string, update: { avatar: string }) => Promise; + updateFile: (params: { file_id: string }) => Promise; +} + +export class S3ImageService { + private deps: S3ImageServiceDeps; + + constructor(deps: S3ImageServiceDeps) { + this.deps = deps; + } + + async uploadImageToS3({ + req, + file, + file_id, + endpoint, + resolution = 'high', + basePath = defaultBasePath, + }: UploadImageParams): Promise { + const inputFilePath = file.path; + try { + if (!req.user) { + throw new Error('[S3ImageService.uploadImageToS3] User not authenticated'); + } + + const appConfig = req.config; + const inputBuffer = await fs.promises.readFile(inputFilePath); + + const { + buffer: resizedBuffer, + width, + height, + } = await this.deps.resizeImageBuffer(inputBuffer, resolution, endpoint); + + const extension = path.extname(inputFilePath); + const userId = req.user.id; + + let processedBuffer: Buffer; + let fileName = `${file_id}__${path.basename(inputFilePath)}`; + const targetExtension = `.${appConfig?.imageOutputType ?? 'webp'}`; + + if (extension.toLowerCase() === targetExtension) { + processedBuffer = resizedBuffer; + } else { + const outputFormat = (appConfig?.imageOutputType ?? 'webp') as keyof FormatEnum; + processedBuffer = await sharp(resizedBuffer).toFormat(outputFormat).toBuffer(); + fileName = fileName.replace(new RegExp(path.extname(fileName) + '$'), targetExtension); + if (!path.extname(fileName)) { + fileName += targetExtension; + } + } + + const downloadURL = await saveBufferToS3({ + userId, + buffer: processedBuffer, + fileName, + basePath, + }); + const bytes = processedBuffer.length; + return { filepath: downloadURL, bytes, width, height }; + } catch (error) { + logger.error( + '[S3ImageService.uploadImageToS3] Error uploading image to S3:', + (error as Error).message, + ); + throw error; + } finally { + await fs.promises + .unlink(inputFilePath) + .catch((e: unknown) => + logger.error( + '[S3ImageService.uploadImageToS3] Failed to delete temp file:', + (e as Error).message, + ), + ); + } + } + + async prepareImageURL(file: { file_id: string; filepath: string }): Promise<[TFile, string]> { + try { + return await Promise.all([this.deps.updateFile({ file_id: file.file_id }), file.filepath]); + } catch (error) { + logger.error( + '[S3ImageService.prepareImageURL] Error preparing image URL:', + (error as Error).message, + ); + throw error; + } + } + + async processAvatar({ + buffer, + userId, + manual, + agentId, + basePath = defaultBasePath, + }: ProcessAvatarParams): Promise { + try { + const metadata = await sharp(buffer).metadata(); + const extension = metadata.format ?? 'png'; + const timestamp = new Date().getTime(); + + const fileName = agentId + ? `agent-${agentId}-avatar-${timestamp}.${extension}` + : `avatar-${timestamp}.${extension}`; + + const downloadURL = await saveBufferToS3({ userId, buffer, fileName, basePath }); + + if (manual === 'true' && !agentId) { + await this.deps.updateUser(userId, { avatar: downloadURL }); + } + + return downloadURL; + } catch (error) { + logger.error( + '[S3ImageService.processAvatar] Error processing S3 avatar:', + (error as Error).message, + ); + throw error; + } + } +} diff --git a/packages/api/src/storage/s3/index.ts b/packages/api/src/storage/s3/index.ts new file mode 100644 index 0000000000..e700610bba --- /dev/null +++ b/packages/api/src/storage/s3/index.ts @@ -0,0 +1,2 @@ +export * from './crud'; +export * from './images'; diff --git a/packages/api/src/storage/s3/s3Config.ts b/packages/api/src/storage/s3/s3Config.ts new file mode 100644 index 0000000000..766c0cf66e --- /dev/null +++ b/packages/api/src/storage/s3/s3Config.ts @@ -0,0 +1,57 @@ +import { logger } from '@librechat/data-schemas'; +import { isEnabled } from '~/utils/common'; + +const MAX_EXPIRY_SECONDS = 7 * 24 * 60 * 60; // 7 days +const DEFAULT_EXPIRY_SECONDS = 2 * 60; // 2 minutes +const DEFAULT_BASE_PATH = 'images'; + +const parseUrlExpiry = (): number => { + if (process.env.S3_URL_EXPIRY_SECONDS === undefined) { + return DEFAULT_EXPIRY_SECONDS; + } + + const parsed = parseInt(process.env.S3_URL_EXPIRY_SECONDS, 10); + if (isNaN(parsed) || parsed <= 0) { + logger.warn( + `[S3] Invalid S3_URL_EXPIRY_SECONDS value: "${process.env.S3_URL_EXPIRY_SECONDS}". Using ${DEFAULT_EXPIRY_SECONDS}s expiry.`, + ); + return DEFAULT_EXPIRY_SECONDS; + } + + return Math.min(parsed, MAX_EXPIRY_SECONDS); +}; + +const parseRefreshExpiry = (): number | null => { + if (!process.env.S3_REFRESH_EXPIRY_MS) { + return null; + } + + const parsed = parseInt(process.env.S3_REFRESH_EXPIRY_MS, 10); + if (isNaN(parsed) || parsed <= 0) { + logger.warn( + `[S3] Invalid S3_REFRESH_EXPIRY_MS value: "${process.env.S3_REFRESH_EXPIRY_MS}". Using default refresh logic.`, + ); + return null; + } + + logger.info(`[S3] Using custom refresh expiry time: ${parsed}ms`); + return parsed; +}; + +// Internal module config — not part of the public @librechat/api surface +export const s3Config = { + /** AWS region for S3 */ + AWS_REGION: process.env.AWS_REGION ?? '', + /** S3 bucket name */ + AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME ?? '', + /** Custom endpoint URL (for MinIO, R2, etc.) */ + AWS_ENDPOINT_URL: process.env.AWS_ENDPOINT_URL, + /** Use path-style URLs instead of virtual-hosted-style */ + AWS_FORCE_PATH_STYLE: isEnabled(process.env.AWS_FORCE_PATH_STYLE), + /** Presigned URL expiry in seconds */ + S3_URL_EXPIRY_SECONDS: parseUrlExpiry(), + /** Custom refresh expiry in milliseconds (null = use default buffer logic) */ + S3_REFRESH_EXPIRY_MS: parseRefreshExpiry(), + /** Default base path for file storage */ + DEFAULT_BASE_PATH, +}; diff --git a/packages/api/src/storage/types.ts b/packages/api/src/storage/types.ts new file mode 100644 index 0000000000..314719f38a --- /dev/null +++ b/packages/api/src/storage/types.ts @@ -0,0 +1,60 @@ +import type { ServerRequest } from '~/types'; + +export interface SaveBufferParams { + userId: string; + buffer: Buffer; + fileName: string; + basePath?: string; +} + +export interface GetURLParams { + userId: string; + fileName: string; + basePath?: string; + customFilename?: string | null; + contentType?: string | null; +} + +export interface SaveURLParams { + userId: string; + URL: string; + fileName: string; + basePath?: string; +} + +export interface UploadFileParams { + req: ServerRequest; + file: Express.Multer.File; + file_id: string; + basePath?: string; +} + +export interface UploadImageParams extends UploadFileParams { + endpoint: string; + resolution?: string; +} + +export interface UploadResult { + filepath: string; + bytes: number; +} + +export interface ImageUploadResult extends UploadResult { + width: number; + height: number; +} + +export interface ProcessAvatarParams { + buffer: Buffer; + userId: string; + manual: string; + agentId?: string; + basePath?: string; +} + +export interface S3FileRef { + filepath: string; + source: string; +} + +export type BatchUpdateFn = (files: Array<{ file_id: string; filepath: string }>) => Promise; From dd72b7b17e4aa56beb05143777cedbafcbd83e99 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 16 Mar 2026 08:26:55 -0400 Subject: [PATCH 100/111] =?UTF-8?q?=F0=9F=94=84=20chore:=20Consolidate=20a?= =?UTF-8?q?gent=20model=20imports=20across=20middleware=20and=20tests=20fr?= =?UTF-8?q?om=20rebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated imports for `createAgent` and `getAgent` to streamline access from a unified `~/models` path. - Enhanced test files to reflect the new import structure, ensuring consistency and maintainability across the codebase. - Improved clarity by removing redundant imports and aligning with the latest model updates. --- .../middleware/accessResources/canAccessAgentFromBody.spec.js | 2 +- api/server/routes/files/images.agents.test.js | 2 +- api/server/routes/files/images.js | 4 ++-- api/server/services/Endpoints/agents/initialize.spec.js | 2 +- api/server/services/Files/permissions.spec.js | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/server/middleware/accessResources/canAccessAgentFromBody.spec.js b/api/server/middleware/accessResources/canAccessAgentFromBody.spec.js index 47f1130d13..9e5e0b093a 100644 --- a/api/server/middleware/accessResources/canAccessAgentFromBody.spec.js +++ b/api/server/middleware/accessResources/canAccessAgentFromBody.spec.js @@ -8,7 +8,7 @@ const { const { MongoMemoryServer } = require('mongodb-memory-server'); const { canAccessAgentFromBody } = require('./canAccessAgentFromBody'); const { User, Role, AclEntry } = require('~/db/models'); -const { createAgent } = require('~/models/Agent'); +const { createAgent } = require('~/models'); describe('canAccessAgentFromBody middleware', () => { let mongoServer; diff --git a/api/server/routes/files/images.agents.test.js b/api/server/routes/files/images.agents.test.js index 862ab87d63..f855a436d4 100644 --- a/api/server/routes/files/images.agents.test.js +++ b/api/server/routes/files/images.agents.test.js @@ -10,7 +10,7 @@ const { ResourceType, PrincipalType, } = require('librechat-data-provider'); -const { createAgent } = require('~/models/Agent'); +const { createAgent } = require('~/models'); jest.mock('~/server/services/Files/process', () => ({ processAgentFileUpload: jest.fn().mockImplementation(async ({ res }) => { diff --git a/api/server/routes/files/images.js b/api/server/routes/files/images.js index d5d8f51193..353557dc4f 100644 --- a/api/server/routes/files/images.js +++ b/api/server/routes/files/images.js @@ -10,7 +10,7 @@ const { filterFile, } = require('~/server/services/Files/process'); const { checkPermission } = require('~/server/services/PermissionService'); -const { getAgent } = require('~/models/Agent'); +const db = require('~/models'); const router = express.Router(); @@ -29,7 +29,7 @@ router.post('/', async (req, res) => { req, res, metadata, - getAgent, + getAgent: db.getAgent, checkPermission, }); if (denied) { diff --git a/api/server/services/Endpoints/agents/initialize.spec.js b/api/server/services/Endpoints/agents/initialize.spec.js index 16b41aca65..8027744965 100644 --- a/api/server/services/Endpoints/agents/initialize.spec.js +++ b/api/server/services/Endpoints/agents/initialize.spec.js @@ -58,8 +58,8 @@ jest.mock('~/cache', () => ({ })); const { initializeClient } = require('./initialize'); -const { createAgent } = require('~/models/Agent'); const { User, AclEntry } = require('~/db/models'); +const { createAgent } = require('~/models'); const PRIMARY_ID = 'agent_primary'; const TARGET_ID = 'agent_target'; diff --git a/api/server/services/Files/permissions.spec.js b/api/server/services/Files/permissions.spec.js index 85e7b2dc5b..c926e83464 100644 --- a/api/server/services/Files/permissions.spec.js +++ b/api/server/services/Files/permissions.spec.js @@ -6,14 +6,14 @@ jest.mock('~/server/services/PermissionService', () => ({ checkPermission: jest.fn(), })); -jest.mock('~/models/Agent', () => ({ +jest.mock('~/models', () => ({ getAgent: jest.fn(), })); const { logger } = require('@librechat/data-schemas'); const { Constants, PermissionBits, ResourceType } = require('librechat-data-provider'); const { checkPermission } = require('~/server/services/PermissionService'); -const { getAgent } = require('~/models/Agent'); +const { getAgent } = require('~/models'); const { filterFilesByAgentAccess, hasAccessToFilesViaAgent } = require('./permissions'); const AUTHOR_ID = 'author-user-id'; From 67db0c1cb36352d106ee4258950078d075b369c4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 16 Mar 2026 09:07:30 -0400 Subject: [PATCH 101/111] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20chore:=20Remove?= =?UTF-8?q?=20Action=20Test=20Suite=20and=20Update=20Mock=20Implementation?= =?UTF-8?q?s=20(#12268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Deleted the Action test suite located in `api/models/Action.spec.js` to streamline the codebase. - Updated various test files to reflect changes in model mocks, consolidating mock implementations for user-related actions and enhancing clarity. - Improved consistency in test setups by aligning with the latest model updates and removing redundant mock definitions. --- api/models/Action.spec.js | 250 ------------------ .../controllers/__tests__/deleteUser.spec.js | 46 ++-- .../agents/filterAuthorizedTools.spec.js | 31 +-- api/server/controllers/agents/v1.js | 4 +- .../__test-utils__/convos-route-mocks.js | 5 + .../convos-duplicate-ratelimit.spec.js | 8 +- .../routes/__tests__/messages-delete.spec.js | 7 +- 7 files changed, 40 insertions(+), 311 deletions(-) delete mode 100644 api/models/Action.spec.js diff --git a/api/models/Action.spec.js b/api/models/Action.spec.js deleted file mode 100644 index 61a3b10f0f..0000000000 --- a/api/models/Action.spec.js +++ /dev/null @@ -1,250 +0,0 @@ -const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { actionSchema } = require('@librechat/data-schemas'); -const { updateAction, getActions, deleteAction } = require('./Action'); - -let mongoServer; - -beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - if (!mongoose.models.Action) { - mongoose.model('Action', actionSchema); - } - await mongoose.connect(mongoUri); -}, 20000); - -afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); -}); - -beforeEach(async () => { - await mongoose.models.Action.deleteMany({}); -}); - -const userId = new mongoose.Types.ObjectId(); - -describe('Action ownership scoping', () => { - describe('updateAction', () => { - it('updates when action_id and agent_id both match', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_1', - agent_id: 'agent_A', - metadata: { domain: 'example.com' }, - }); - - const result = await updateAction( - { action_id: 'act_1', agent_id: 'agent_A' }, - { metadata: { domain: 'updated.com' } }, - ); - - expect(result).not.toBeNull(); - expect(result.metadata.domain).toBe('updated.com'); - expect(result.agent_id).toBe('agent_A'); - }); - - it('does not update when agent_id does not match (creates a new doc via upsert)', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_1', - agent_id: 'agent_B', - metadata: { domain: 'victim.com', api_key: 'secret' }, - }); - - const result = await updateAction( - { action_id: 'act_1', agent_id: 'agent_A' }, - { user: userId, metadata: { domain: 'attacker.com' } }, - ); - - expect(result.metadata.domain).toBe('attacker.com'); - - const original = await mongoose.models.Action.findOne({ - action_id: 'act_1', - agent_id: 'agent_B', - }).lean(); - expect(original).not.toBeNull(); - expect(original.metadata.domain).toBe('victim.com'); - expect(original.metadata.api_key).toBe('secret'); - }); - - it('updates when action_id and assistant_id both match', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_2', - assistant_id: 'asst_X', - metadata: { domain: 'example.com' }, - }); - - const result = await updateAction( - { action_id: 'act_2', assistant_id: 'asst_X' }, - { metadata: { domain: 'updated.com' } }, - ); - - expect(result).not.toBeNull(); - expect(result.metadata.domain).toBe('updated.com'); - }); - - it('does not overwrite when assistant_id does not match', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_2', - assistant_id: 'asst_victim', - metadata: { domain: 'victim.com', api_key: 'secret' }, - }); - - await updateAction( - { action_id: 'act_2', assistant_id: 'asst_attacker' }, - { user: userId, metadata: { domain: 'attacker.com' } }, - ); - - const original = await mongoose.models.Action.findOne({ - action_id: 'act_2', - assistant_id: 'asst_victim', - }).lean(); - expect(original).not.toBeNull(); - expect(original.metadata.domain).toBe('victim.com'); - expect(original.metadata.api_key).toBe('secret'); - }); - }); - - describe('deleteAction', () => { - it('deletes when action_id and agent_id both match', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_del', - agent_id: 'agent_A', - metadata: { domain: 'example.com' }, - }); - - const result = await deleteAction({ action_id: 'act_del', agent_id: 'agent_A' }); - expect(result).not.toBeNull(); - expect(result.action_id).toBe('act_del'); - - const remaining = await mongoose.models.Action.countDocuments(); - expect(remaining).toBe(0); - }); - - it('returns null and preserves the document when agent_id does not match', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_del', - agent_id: 'agent_B', - metadata: { domain: 'victim.com' }, - }); - - const result = await deleteAction({ action_id: 'act_del', agent_id: 'agent_A' }); - expect(result).toBeNull(); - - const remaining = await mongoose.models.Action.countDocuments(); - expect(remaining).toBe(1); - }); - - it('deletes when action_id and assistant_id both match', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_del_asst', - assistant_id: 'asst_X', - metadata: { domain: 'example.com' }, - }); - - const result = await deleteAction({ action_id: 'act_del_asst', assistant_id: 'asst_X' }); - expect(result).not.toBeNull(); - - const remaining = await mongoose.models.Action.countDocuments(); - expect(remaining).toBe(0); - }); - - it('returns null and preserves the document when assistant_id does not match', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_del_asst', - assistant_id: 'asst_victim', - metadata: { domain: 'victim.com' }, - }); - - const result = await deleteAction({ - action_id: 'act_del_asst', - assistant_id: 'asst_attacker', - }); - expect(result).toBeNull(); - - const remaining = await mongoose.models.Action.countDocuments(); - expect(remaining).toBe(1); - }); - }); - - describe('getActions (unscoped baseline)', () => { - it('returns actions by action_id regardless of agent_id', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_shared', - agent_id: 'agent_B', - metadata: { domain: 'example.com' }, - }); - - const results = await getActions({ action_id: 'act_shared' }, true); - expect(results).toHaveLength(1); - expect(results[0].agent_id).toBe('agent_B'); - }); - - it('returns actions scoped by agent_id when provided', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_scoped', - agent_id: 'agent_A', - metadata: { domain: 'a.com' }, - }); - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_other', - agent_id: 'agent_B', - metadata: { domain: 'b.com' }, - }); - - const results = await getActions({ agent_id: 'agent_A' }); - expect(results).toHaveLength(1); - expect(results[0].action_id).toBe('act_scoped'); - }); - }); - - describe('cross-type protection', () => { - it('updateAction with agent_id filter does not overwrite assistant-owned action', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_cross', - assistant_id: 'asst_victim', - metadata: { domain: 'victim.com', api_key: 'secret' }, - }); - - await updateAction( - { action_id: 'act_cross', agent_id: 'agent_attacker' }, - { user: userId, metadata: { domain: 'evil.com' } }, - ); - - const original = await mongoose.models.Action.findOne({ - action_id: 'act_cross', - assistant_id: 'asst_victim', - }).lean(); - expect(original).not.toBeNull(); - expect(original.metadata.domain).toBe('victim.com'); - expect(original.metadata.api_key).toBe('secret'); - }); - - it('deleteAction with agent_id filter does not delete assistant-owned action', async () => { - await mongoose.models.Action.create({ - user: userId, - action_id: 'act_cross_del', - assistant_id: 'asst_victim', - metadata: { domain: 'victim.com' }, - }); - - const result = await deleteAction({ action_id: 'act_cross_del', agent_id: 'agent_attacker' }); - expect(result).toBeNull(); - - const remaining = await mongoose.models.Action.countDocuments(); - expect(remaining).toBe(1); - }); - }); -}); diff --git a/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js index 6382cd1d8e..8dcd217657 100644 --- a/api/server/controllers/__tests__/deleteUser.spec.js +++ b/api/server/controllers/__tests__/deleteUser.spec.js @@ -35,6 +35,8 @@ jest.mock('@librechat/api', () => ({ MCPTokenStorage: {}, normalizeHttpError: jest.fn(), extractWebSearchEnvVars: jest.fn(), + needsRefresh: jest.fn(), + getNewS3URL: jest.fn(), })); jest.mock('~/models', () => ({ @@ -51,20 +53,19 @@ jest.mock('~/models', () => ({ updateUser: (...args) => mockUpdateUser(...args), findToken: (...args) => mockFindToken(...args), getFiles: (...args) => mockGetFiles(...args), -})); - -jest.mock('~/db/models', () => ({ - ConversationTag: { deleteMany: jest.fn() }, - AgentApiKey: { deleteMany: jest.fn() }, - Transaction: { deleteMany: jest.fn() }, - MemoryEntry: { deleteMany: jest.fn() }, - Assistant: { deleteMany: jest.fn() }, - AclEntry: { deleteMany: jest.fn() }, - Balance: { deleteMany: jest.fn() }, - Action: { deleteMany: jest.fn() }, - Group: { updateMany: jest.fn() }, - Token: { deleteMany: jest.fn() }, - User: {}, + deleteToolCalls: (...args) => mockDeleteToolCalls(...args), + deleteUserAgents: (...args) => mockDeleteUserAgents(...args), + deleteUserPrompts: (...args) => mockDeleteUserPrompts(...args), + deleteTransactions: jest.fn(), + deleteBalances: jest.fn(), + deleteAllAgentApiKeys: jest.fn(), + deleteAssistants: jest.fn(), + deleteConversationTags: jest.fn(), + deleteAllUserMemories: jest.fn(), + deleteActions: jest.fn(), + deleteTokens: jest.fn(), + removeUserFromAllGroups: jest.fn(), + deleteAclEntries: jest.fn(), })); jest.mock('~/server/services/PluginService', () => ({ @@ -91,11 +92,6 @@ jest.mock('~/server/services/Config/getCachedTools', () => ({ invalidateCachedTools: jest.fn(), })); -jest.mock('~/server/services/Files/S3/crud', () => ({ - needsRefresh: jest.fn(), - getNewS3URL: jest.fn(), -})); - jest.mock('~/server/services/Files/process', () => ({ processDeleteRequest: (...args) => mockProcessDeleteRequest(...args), })); @@ -108,18 +104,6 @@ jest.mock('~/server/services/PermissionService', () => ({ getSoleOwnedResourceIds: jest.fn().mockResolvedValue([]), })); -jest.mock('~/models/ToolCall', () => ({ - deleteToolCalls: (...args) => mockDeleteToolCalls(...args), -})); - -jest.mock('~/models/Prompt', () => ({ - deleteUserPrompts: (...args) => mockDeleteUserPrompts(...args), -})); - -jest.mock('~/models/Agent', () => ({ - deleteUserAgents: (...args) => mockDeleteUserAgents(...args), -})); - jest.mock('~/cache', () => ({ getLogStores: jest.fn(), })); diff --git a/api/server/controllers/agents/filterAuthorizedTools.spec.js b/api/server/controllers/agents/filterAuthorizedTools.spec.js index 259e41fb0d..e215fdc1fc 100644 --- a/api/server/controllers/agents/filterAuthorizedTools.spec.js +++ b/api/server/controllers/agents/filterAuthorizedTools.spec.js @@ -22,10 +22,6 @@ jest.mock('~/config', () => ({ })), })); -jest.mock('~/models/Project', () => ({ - getProjectByName: jest.fn().mockResolvedValue(null), -})); - jest.mock('~/server/services/Files/strategies', () => ({ getStrategyFunctions: jest.fn(), })); @@ -34,23 +30,10 @@ jest.mock('~/server/services/Files/images/avatar', () => ({ resizeAvatar: jest.fn(), })); -jest.mock('~/server/services/Files/S3/crud', () => ({ - refreshS3Url: jest.fn(), -})); - jest.mock('~/server/services/Files/process', () => ({ filterFile: jest.fn(), })); -jest.mock('~/models/Action', () => ({ - updateAction: jest.fn(), - getActions: jest.fn().mockResolvedValue([]), -})); - -jest.mock('~/models/File', () => ({ - deleteFileByFilter: jest.fn(), -})); - jest.mock('~/server/services/PermissionService', () => ({ findAccessibleResources: jest.fn().mockResolvedValue([]), findPubliclyAccessibleResources: jest.fn().mockResolvedValue([]), @@ -59,9 +42,17 @@ jest.mock('~/server/services/PermissionService', () => ({ checkPermission: jest.fn().mockResolvedValue(true), })); -jest.mock('~/models', () => ({ - getCategoriesWithCounts: jest.fn(), -})); +jest.mock('~/models', () => { + const mongoose = require('mongoose'); + const { createModels, createMethods } = require('@librechat/data-schemas'); + createModels(mongoose); + const methods = createMethods(mongoose); + return { + ...methods, + getCategoriesWithCounts: jest.fn(), + deleteFileByFilter: jest.fn(), + }; +}); jest.mock('~/cache', () => ({ getLogStores: jest.fn(() => ({ diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index b6eb4fc22c..17985f97ce 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -66,9 +66,7 @@ const validateEdgeAgentAccess = async (edges, userId, userRole) => { return []; } - const agents = (await Promise.all([...edgeAgentIds].map((id) => getAgent({ id })))).filter( - Boolean, - ); + const agents = await db.getAgents({ id: { $in: [...edgeAgentIds] } }); if (agents.length === 0) { return []; diff --git a/api/server/routes/__test-utils__/convos-route-mocks.js b/api/server/routes/__test-utils__/convos-route-mocks.js index f89b77db3f..0929e0759d 100644 --- a/api/server/routes/__test-utils__/convos-route-mocks.js +++ b/api/server/routes/__test-utils__/convos-route-mocks.js @@ -48,8 +48,13 @@ module.exports = { toolCallModel: () => ({ deleteToolCalls: jest.fn() }), sharedModels: () => ({ + getConvosByCursor: jest.fn(), + getConvo: jest.fn(), + deleteConvos: jest.fn(), + saveConvo: jest.fn(), deleteAllSharedLinks: jest.fn(), deleteConvoSharedLink: jest.fn(), + deleteToolCalls: jest.fn(), }), requireJwtAuth: () => (req, res, next) => next(), diff --git a/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js b/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js index 788119a569..a75c11ccba 100644 --- a/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js +++ b/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js @@ -12,9 +12,11 @@ jest.mock('librechat-data-provider', () => jest.mock('~/cache/logViolation', () => jest.fn().mockResolvedValue(undefined)); jest.mock('~/cache/getLogStores', () => require(MOCKS).logStores()); -jest.mock('~/models/Conversation', () => require(MOCKS).conversationModel()); -jest.mock('~/models/ToolCall', () => require(MOCKS).toolCallModel()); -jest.mock('~/models', () => require(MOCKS).sharedModels()); +jest.mock('~/models', () => ({ + ...require(MOCKS).sharedModels(), + ...require(MOCKS).conversationModel(), + ...require(MOCKS).toolCallModel(), +})); jest.mock('~/server/middleware/requireJwtAuth', () => require(MOCKS).requireJwtAuth()); jest.mock('~/server/middleware', () => { diff --git a/api/server/routes/__tests__/messages-delete.spec.js b/api/server/routes/__tests__/messages-delete.spec.js index e134eecfd0..714d497719 100644 --- a/api/server/routes/__tests__/messages-delete.spec.js +++ b/api/server/routes/__tests__/messages-delete.spec.js @@ -34,6 +34,9 @@ jest.mock('~/models', () => ({ getMessages: jest.fn(), updateMessage: jest.fn(), deleteMessages: jest.fn(), + getConvosQueried: jest.fn(), + searchMessages: jest.fn(), + getMessagesByCursor: jest.fn(), })); jest.mock('~/server/services/Artifacts/update', () => ({ @@ -48,10 +51,6 @@ jest.mock('~/server/middleware', () => ({ validateMessageReq: (req, res, next) => next(), })); -jest.mock('~/models/Conversation', () => ({ - getConvosQueried: jest.fn(), -})); - jest.mock('~/db/models', () => ({ Message: { findOne: jest.fn(), From b5c097e5c7c7cfa84e322fb87169b888e754e0e6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 21 Mar 2026 12:03:10 -0400 Subject: [PATCH 102/111] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20feat:=20Agent=20Co?= =?UTF-8?q?ntext=20Compaction/Summarization=20(#12287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: imports/types Add summarization config and package-level summarize handler contracts Register summarize handlers across server controller paths Port cursor dual-read/dual-write summary support and UI status handling Selectively merge cursor branch files for BaseClient summary content block detection (last-summary-wins), dual-write persistence, summary block unit tests, and on_summarize_status SSE event handling with started/completed/failed branches. Co-authored-by: Cursor refactor: type safety feat: add localization for summarization status messages refactor: optimize summary block detection in BaseClient Updated the logic for identifying existing summary content blocks to use a reverse loop for improved efficiency. Added a new test case to ensure the last summary content block is updated correctly when multiple summary blocks exist. chore: add runName to chainOptions in AgentClient refactor: streamline summarization configuration and handler integration Removed the deprecated summarizeNotConfigured function and replaced it with a more flexible createSummarizeFn. Updated the summarization handler setup across various controllers to utilize the new function, enhancing error handling and configuration resolution. Improved overall code clarity and maintainability by consolidating summarization logic. feat(summarization): add staged chunk-and-merge fallback feat(usage): track summarization usage separately from messages feat(summarization): resolve prompt from config in runtime fix(endpoints): use @librechat/api provider config loader refactor(agents): import getProviderConfig from @librechat/api chore: code order feat(app-config): auto-enable summarization when configured feat: summarization config refactor(summarization): streamline persist summary handling and enhance configuration validation Removed the deprecated createDeferredPersistSummary function and integrated a new createPersistSummary function for MongoDB persistence. Updated summarization handlers across various controllers to utilize the new persistence method. Enhanced validation for summarization configuration to ensure provider, model, and prompt are properly set, improving error handling and overall robustness. refactor(summarization): update event handling and remove legacy summarize handlers Replaced the deprecated summarization handlers with new event-driven handlers for summarization start and completion across multiple controllers. This change enhances the clarity of the summarization process and improves the integration of summarization events in the application. Additionally, removed unused summarization functions and streamlined the configuration loading process. refactor(summarization): standardize event names in handlers Updated event names in the summarization handlers to use constants from GraphEvents for consistency and clarity. This change improves maintainability and reduces the risk of errors related to string literals in event handling. feat(summarization): enhance usage tracking for summarization events Added logic to track summarization usage in multiple controllers by checking the current node type. If the node indicates a summarization task, the usage type is set accordingly. This change improves the granularity of usage data collected during summarization processes. feat(summarization): integrate SummarizationConfig into AppSummarizationConfig type Enhanced the AppSummarizationConfig type by extending it with the SummarizationConfig type from librechat-data-provider. This change improves type safety and consistency in the summarization configuration structure. test: add end-to-end tests for summarization functionality Introduced a comprehensive suite of end-to-end tests for the summarization feature, covering the full LibreChat pipeline from message creation to summarization. This includes a new setup file for environment configuration and a Jest configuration specifically for E2E tests. The tests utilize real API keys and ensure proper integration with the summarization process, enhancing overall test coverage and reliability. refactor(summarization): include initial summary in formatAgentMessages output Updated the formatAgentMessages function to return an initial summary alongside messages and index token count map. This change is reflected in multiple controllers and the corresponding tests, enhancing the summarization process by providing additional context for each agent's response. refactor: move hydrateMissingIndexTokenCounts to tokenMap utility Extracted the hydrateMissingIndexTokenCounts function from the AgentClient and related tests into a new tokenMap utility file. This change improves code organization and reusability, allowing for better management of token counting logic across the application. refactor(summarization): standardize step event handling and improve summary rendering Refactored the step event handling in the useStepHandler and related components to utilize constants for event names, enhancing consistency and maintainability. Additionally, improved the rendering logic in the Summary component to conditionally display the summary text based on its availability, providing a better user experience during the summarization process. feat(summarization): introduce baseContextTokens and reserveTokensRatio for improved context management Added baseContextTokens to the InitializedAgent type to calculate the context budget based on agentMaxContextNum and maxOutputTokensNum. Implemented reserveTokensRatio in the createRun function to allow configurable context token management. Updated related tests to validate these changes and ensure proper functionality. feat(summarization): add minReserveTokens, context pruning, and overflow recovery configurations Introduced new configuration options for summarization, including minReserveTokens, context pruning settings, and overflow recovery parameters. Updated the createRun function to accommodate these new options and added a comprehensive test suite to validate their functionality and integration within the summarization process. feat(summarization): add updatePrompt and reserveTokensRatio to summarization configuration Introduced an updatePrompt field for updating existing summaries with new messages, enhancing the flexibility of the summarization process. Additionally, added reserveTokensRatio to the configuration schema, allowing for improved management of token allocation during summarization. Updated related tests to validate these new features. feat(logging): add on_agent_log event handler for structured logging Implemented an on_agent_log event handler in both the agents' callbacks and responses to facilitate structured logging of agent activities. This enhancement allows for better tracking and debugging of agent interactions by logging messages with associated metadata. Updated the summarization process to ensure proper handling of log events. fix: remove duplicate IBalanceUpdate interface declaration perf(usage): single-pass partition of collectedUsage Replace two Array.filter() passes with a single for-of loop that partitions message vs. summarization usages in one iteration. fix(BaseClient): shallow-copy message content before mutating and preserve string content Avoid mutating the original message.content array in-place when appending a summary block. Also convert string content to a text content part instead of silently discarding it. fix(ui): fix Part.tsx indentation and useStepHandler summarize-complete handling - Fix SUMMARY else-if branch indentation in Part.tsx to match chain level - Guard ON_SUMMARIZE_COMPLETE with didFinalize flag to avoid unnecessary re-renders when no summarizing parts exist - Protect against undefined completeData.summary instead of unsafe spread fix(agents): use strict enabled check for summarization handlers Change summarizationConfig?.enabled !== false to === true so handlers are not registered when summarizationConfig is undefined. chore: fix initializeClient JSDoc and move DEFAULT_RESERVE_RATIO to module scope refactor(Summary): align collapse/expand behavior with Reasoning component - Single render path instead of separate streaming vs completed branches - Use useMessageContext for isSubmitting/isLatestMessage awareness so the "Summarizing..." label only shows during active streaming - Default to collapsed (matching Reasoning), user toggles to expand - Add proper aria attributes (aria-hidden, role, aria-controls, contentId) - Hide copy button while actively streaming feat(summarization): default to self-summarize using agent's own provider/model When no summarization config is provided (neither in librechat.yaml nor on the agent), automatically enable summarization using the agent's own provider and model. The agents package already provides default prompts, so no prompt configuration is needed. Also removes the dead resolveSummarizationLLMConfig in summarize.ts (and its spec) — run.ts buildAgentContext is the single source of truth for summarization config resolution. Removes the duplicate RuntimeSummarizationConfig local type in favor of the canonical SummarizationConfig from data-provider. chore: schema and type cleanup for summarization - Add trigger field to summarizationAgentOverrideSchema so per-agent trigger overrides in librechat.yaml are not silently stripped by Zod - Remove unused SummarizationStatus type from runs.ts - Make AppSummarizationConfig.enabled non-optional to reflect the invariant that loadSummarizationConfig always sets it refactor(responses): extract duplicated on_agent_log handler refactor(run): use agents package types for summarization config Import SummarizationConfig, ContextPruningConfig, and OverflowRecoveryConfig from @librechat/agents and use them to type-check the translation layer in buildAgentContext. This ensures the config object passed to the agent graph matches what it expects. - Use `satisfies AgentSummarizationConfig` on the config object - Cast contextPruningConfig and overflowRecoveryConfig to agents types - Properly narrow trigger fields from DeepPartial to required shape feat(config): add maxToolResultChars to base endpoint schema Add maxToolResultChars to baseEndpointSchema so it can be configured on any endpoint in librechat.yaml. Resolved during agent initialization using getProviderConfig's endpoint resolution: custom endpoint config takes precedence, then the provider-specific endpoint config, then the shared `all` config. Passed through to the agents package ToolNode, which uses it to cap tool result length before it enters the context window. When not configured, the agents package computes a sensible default from maxContextTokens. fix(summarization): forward agent model_parameters in self-summarize default When no explicit summarization config exists, the self-summarize default now forwards the agent's model_parameters as the summarization parameters. This ensures provider-specific settings (e.g. Bedrock region, credentials, endpoint host) are available when the agents package constructs the summarization LLM. fix(agents): register summarization handlers by default Change the enabled gate from === true to !== false so handlers register when no explicit summarization config exists. This aligns with the self-summarize default where summarization is always on unless explicitly disabled via enabled: false. refactor(summarization): let agents package inherit clientOptions for self-summarize Remove model_parameters forwarding from the self-summarize default. The agents package now reuses the agent's own clientOptions when the summarization provider matches the agent's provider, inheriting all provider-specific settings (region, credentials, proxy, etc.) automatically. refactor(summarization): use MessageContentComplex[] for summary content Unify summary content to always use MessageContentComplex[] arrays, matching the pattern used by on_message_delta. No more string | array unions — content is always an array of typed blocks ({ type: 'text', text: '...' } for text, { type: 'reasoning_content', ... } for reasoning). Agents package: - SummaryContentBlock.content: MessageContentComplex[] (was string) - tokenCount now optional (not sent on deltas) - Removed reasoning field — reasoning is now a content block type - streamAndCollect normalizes all chunks to content block arrays - Delta events pass content blocks directly LibreChat: - SummaryContentPart.content: Agents.MessageContentComplex[] - Updated Part.tsx, Summary.tsx, useStepHandler.ts, BaseClient.js - Summary.tsx derives display text from content blocks via useMemo - Aggregator uses simple array spread refactor(summarization): enhance summary handling and text extraction - Updated BaseClient.js to improve summary text extraction, accommodating both legacy and new content formats. - Modified summarization logic to ensure consistent handling of summary content across different message formats. - Adjusted test cases in summarization.e2e.spec.js to utilize the new summary text extraction method. - Refined SSE useStepHandler to initialize summary content as an array. - Updated configuration schema by removing unused minReserveTokens field. - Cleaned up SummaryContentPart type by removing rangeHash property. These changes streamline the summarization process and ensure compatibility with various content structures. refactor(summarization): streamline usage tracking and logging - Removed direct checks for summarization nodes in ModelEndHandler and replaced them with a dedicated markSummarizationUsage function for better readability and maintainability. - Updated OpenAIChatCompletionController and responses handlers to utilize the new markSummarizationUsage function for setting usage types. - Enhanced logging functionality by ensuring the logger correctly handles different log levels. - Introduced a new useCopyToClipboard hook in the Summary component to encapsulate clipboard copy logic, improving code reusability and clarity. These changes improve the overall structure and efficiency of the summarization handling and logging processes. refactor(summarization): update summary content block documentation - Removed outdated comment regarding the last summary content block in BaseClient.js. - Added a new comment to clarify the purpose of the findSummaryContentBlock method, ensuring consistency in documentation. These changes enhance code clarity and maintainability by providing accurate descriptions of the summarization logic. refactor(summarization): update summary content structure in tests - Modified the summarization content structure in e2e tests to use an array format for text, aligning with recent changes in summary handling. - Updated test descriptions to clarify the behavior of context token calculations, ensuring consistency and clarity in the tests. These changes enhance the accuracy and maintainability of the summarization tests by reflecting the updated content structure. refactor(summarization): remove legacy E2E test setup and configuration - Deleted the e2e-setup.js and jest.e2e.config.js files, which contained legacy configurations for E2E tests using real API keys. - Introduced a new summarization.e2e.ts file that implements comprehensive E2E backend integration tests for the summarization process, utilizing real AI providers and tracking summaries throughout the run. These changes streamline the testing framework by consolidating E2E tests into a single, more robust file while removing outdated configurations. refactor(summarization): enhance E2E tests and error handling - Added a cleanup step to force exit after all tests to manage Redis connections. - Updated the summarization model to 'claude-haiku-4-5-20251001' for consistency across tests. - Improved error handling in the processStream function to capture and return processing errors. - Enhanced logging for cross-run tests and tight context scenarios to provide better insights into test execution. These changes improve the reliability and clarity of the E2E tests for the summarization process. refactor(summarization): enhance test coverage for maxContextTokens behavior - Updated run-summarization.test.ts to include a new test case ensuring that maxContextTokens does not exceed user-defined limits, even when calculated ratios suggest otherwise. - Modified summarization.e2e.ts to replace legacy UsageMetadata type with a more appropriate type for collectedUsage, improving type safety and clarity in the test setup. These changes improve the robustness of the summarization tests by validating context token constraints and refining type definitions. feat(summarization): add comprehensive E2E tests for summarization process - Introduced a new summarization.e2e.test.ts file that implements extensive end-to-end integration tests for the summarization pipeline, covering the full flow from LibreChat to agents. - The tests utilize real AI providers and include functionality to track summaries during and between runs. - Added necessary cleanup steps to manage Redis connections post-tests and ensure proper exit. These changes enhance the testing framework by providing robust coverage for the summarization process, ensuring reliability and performance under real-world conditions. fix(service): import logger from winston configuration - Removed the import statement for logger from '@librechat/data-schemas' and replaced it with an import from '~/config/winston'. - This change ensures that the logger is correctly sourced from the updated configuration, improving consistency in logging practices across the application. refactor(summary): simplify Summary component and enhance token display - Removed the unused `meta` prop from the `SummaryButton` component to streamline its interface. - Updated the token display logic to use a localized string for better internationalization support. - Adjusted the rendering of the `meta` information to improve its visibility within the `Summary` component. These changes enhance the clarity and usability of the Summary component while ensuring better localization practices. feat(summarization): add maxInputTokens configuration for summarization - Introduced a new `maxInputTokens` property in the summarization configuration schema to control the amount of conversation context sent to the summarizer, with a default value of 10000. - Updated the `createRun` function to utilize the new `maxInputTokens` setting, allowing for more flexible summarization based on agent context. These changes enhance the summarization capabilities by providing better control over input token limits, improving the overall summarization process. refactor(summarization): simplify maxInputTokens logic in createRun function - Updated the logic for the `maxInputTokens` property in the `createRun` function to directly use the agent's base context tokens when the resolved summarization configuration does not specify a value. - This change streamlines the configuration process and enhances clarity in how input token limits are determined for summarization. These modifications improve the maintainability of the summarization configuration by reducing complexity in the token calculation logic. feat(summary): enhance Summary component to display meta information - Updated the SummaryContent component to accept an optional `meta` prop, allowing for additional contextual information to be displayed above the main content. - Adjusted the rendering logic in the Summary component to utilize the new `meta` prop, improving the visibility of supplementary details. These changes enhance the user experience by providing more context within the Summary component, making it clearer and more informative. refactor(summarization): standardize reserveRatio configuration in summarization logic - Replaced instances of `reserveTokensRatio` with `reserveRatio` in the `createRun` function and related tests to unify the terminology across the codebase. - Updated the summarization configuration schema to reflect this change, ensuring consistency in how the reserve ratio is defined and utilized. - Removed the per-agent override logic for summarization configuration, simplifying the overall structure and enhancing clarity. These modifications improve the maintainability and readability of the summarization logic by standardizing the configuration parameters. * fix: circular dependency of `~/models` * chore: update logging scope in agent log handlers Changed log scope from `[agentus:${data.scope}]` to `[agents:${data.scope}]` in both the callbacks and responses controllers to ensure consistent logging format across the application. * feat: calibration ratio * refactor(tests): update summarizationConfig tests to reflect changes in enabled property Modified tests to check for the new `summarizationEnabled` property instead of the deprecated `enabled` field in the summarization configuration. This change ensures that the tests accurately validate the current configuration structure and behavior of the agents. * feat(tests): add markSummarizationUsage mock for improved test coverage Introduced a mock for the markSummarizationUsage function in the responses unit tests to enhance the testing of summarization usage tracking. This addition supports better validation of summarization-related functionalities and ensures comprehensive test coverage for the agents' response handling. * refactor(tests): simplify event handler setup in createResponse tests Removed redundant mock implementations for event handlers in the createResponse unit tests, streamlining the setup process. This change enhances test clarity and maintainability while ensuring that the tests continue to validate the correct behavior of usage tracking during on_chat_model_end events. * refactor(agents): move calibration ratio capture to finally block Reorganized the logic for capturing the calibration ratio in the AgentClient class to ensure it is executed in the finally block. This change guarantees that the ratio is captured even if the run is aborted, enhancing the reliability of the response message persistence. Removed redundant code and improved clarity in the handling of context metadata. * refactor(agents): streamline bulk write logic in recordCollectedUsage function Removed redundant bulk write operations and consolidated document handling in the recordCollectedUsage function. The logic now combines all documents into a single bulk write operation, improving efficiency and reducing error handling complexity. Updated logging to provide consistent error messages for bulk write failures. * refactor(agents): enhance summarization configuration resolution in createRun function Streamlined the summarization configuration logic by introducing a base configuration and allowing for overrides from agent-specific settings. This change improves clarity and maintainability, ensuring that the summarization configuration is consistently applied while retaining flexibility for customization. Updated the handling of summarization parameters to ensure proper integration with the agent's model and provider settings. * refactor(agents): remove unused tokenCountMap and streamline calibration ratio handling Eliminated the unused tokenCountMap variable from the AgentClient class to enhance code clarity. Additionally, streamlined the logic for capturing the calibration ratio by using optional chaining and a fallback value, ensuring that context metadata is consistently defined. This change improves maintainability and reduces potential confusion in the codebase. * refactor(agents): extract agent log handler for improved clarity and reusability Refactored the agent log handling logic by extracting it into a dedicated function, `agentLogHandler`, enhancing code clarity and reusability across different modules. Updated the event handlers in both the OpenAI and responses controllers to utilize the new handler, ensuring consistent logging behavior throughout the application. * test: add summarization event tests for useStepHandler Implemented a series of tests for the summarization events in the useStepHandler hook. The tests cover scenarios for ON_SUMMARIZE_START, ON_SUMMARIZE_DELTA, and ON_SUMMARIZE_COMPLETE events, ensuring proper handling of summarization logic, including message accumulation and finalization. This addition enhances test coverage and validates the correct behavior of the summarization process within the application. * refactor(config): update summarizationTriggerSchema to use enum for type validation Changed the type of the `type` field in the summarizationTriggerSchema from a string to an enum with a single value 'token_count'. This modification enhances type safety and ensures that only valid types are accepted in the configuration, improving overall clarity and maintainability of the schema. * test(usage): add bulk write tests for message and summarization usage Implemented tests for the bulk write functionality in the recordCollectedUsage function, covering scenarios for combined message and summarization usage, summarization-only usage, and message-only usage. These tests ensure correct document handling and token rollup calculations, enhancing test coverage and validating the behavior of the usage tracking logic. * refactor(Chat): enhance clipboard copy functionality and type definitions in Summary component Updated the Summary component to improve the clipboard copy functionality by handling clipboard permission errors. Refactored type definitions for SummaryProps to use a more specific type, enhancing type safety. Adjusted the SummaryButton and FloatingSummaryBar components to accept isCopied and onCopy props, promoting better separation of concerns and reusability. * chore(translations): remove unused "Expand Summary" key from English translations Deleted the "Expand Summary" key from the English translation file to streamline the localization resources and improve clarity in the user interface. This change helps maintain an organized and efficient translation structure. * refactor: adjust token counting for Claude model to account for API discrepancies Implemented a correction factor for token counting when using the Claude model, addressing discrepancies between Anthropic's API and local tokenizer results. This change ensures accurate token counts by applying a scaling factor, improving the reliability of token-related functionalities. * refactor(agents): implement token count adjustment for Claude model messages Added a method to adjust token counts for messages processed by the Claude model, applying a correction factor to align with API expectations. This enhancement improves the accuracy of token counting, ensuring reliable functionality when interacting with the Claude model. * refactor(agents): token counting for media content in messages Introduced a new method to estimate token costs for image and document blocks in messages, improving the accuracy of token counting. This enhancement ensures that media content is properly accounted for, particularly for the Claude model, by integrating additional token estimation logic for various content types. Updated the token counting function to utilize this new method, enhancing overall reliability and functionality. * chore: fix missing import * fix(agents): clamp baseContextTokens and document reserve ratio change Prevent negative baseContextTokens when maxOutputTokens exceeds the context window (misconfigured models). Document the 10%→5% default reserve ratio reduction introduced alongside summarization. * fix(agents): include media tokens in hydrated token counts Add estimateMediaTokensForMessage to createTokenCounter so the hydration path (used by hydrateMissingIndexTokenCounts) matches the precomputed path in AgentClient.getTokenCountForMessage. Without this, messages containing images or documents were systematically undercounted during hydration, risking context window overflow. Add 34 unit tests covering all block-type branches of estimateMediaTokensForMessage. * fix(agents): include summarization output tokens in usage return value The returned output_tokens from recordCollectedUsage now reflects all billed LLM calls (message + summarization). Previously, summarization completions were billed but excluded from the returned metadata, causing a discrepancy between what users were charged and what the response message reported. * fix(tests): replace process.exit with proper Redis cleanup in e2e test The summarization E2E test used process.exit(0) to work around a Redis connection opened at import time, which killed the Jest runner and bypassed teardown. Use ioredisClient.quit() and keyvRedisClient.disconnect() for graceful cleanup instead. * fix(tests): update getConvo imports in OpenAI and response tests Refactor test files to import getConvo from the main models module instead of the Conversation submodule. This change ensures consistency across tests and simplifies the import structure, enhancing maintainability. * fix(clients): improve summary text validation in BaseClient Refactor the summary extraction logic to ensure that only non-empty summary texts are considered valid. This change enhances the robustness of the message processing by utilizing a dedicated method for summary text retrieval, improving overall reliability. * fix(config): replace z.any() with explicit union in summarization schema Model parameters (temperature, top_p, etc.) are constrained to primitive types rather than the policy-violating z.any(). * refactor(agents): deduplicate CLAUDE_TOKEN_CORRECTION constant Export from the TS source in packages/api and import in the JS client, eliminating the static class property that could drift out of sync. * refactor(agents): eliminate duplicate selfProvider in buildAgentContext selfProvider and provider were derived from the same expression with different type casts. Consolidated to a single provider variable. * refactor(agents): extract shared SSE handlers and restrict log levels - buildSummarizationHandlers() factory replaces triplicated handler blocks across responses.js and openai.js - agentLogHandlerObj exported from callbacks.js for consistent reuse - agentLogHandler restricted to an allowlist of safe log levels (debug, info, warn, error) instead of accepting arbitrary strings * fix(SSE): batch summarize deltas, add exhaustiveness check, conditional error announcement - ON_SUMMARIZE_DELTA coalesces rapid-fire renders via requestAnimationFrame instead of calling setMessages per chunk - Exhaustive never-check on TStepEvent catches unhandled variants at compile time when new StepEvents are added - ON_SUMMARIZE_COMPLETE error announcement only fires when a summary part was actually present and removed * feat(agents): persist instruction overhead in contextMeta and seed across runs Extend contextMeta with instructionOverhead and toolCount so the provider-observed instruction overhead is persisted on the response message and seeded into the pruner on subsequent runs. This enables the pruner to use a calibrated budget from the first call instead of waiting for a provider observation, preventing the ratio collapse caused by local tokenizer overestimating tool schema tokens. The seeded overhead is only used when encoding and tool count match between runs, ensuring stale values from different configurations are discarded. * test(agents): enhance OpenAI test mocks for summarization handlers Updated the OpenAI test suite to include additional mock implementations for summarization handlers, including buildSummarizationHandlers, markSummarizationUsage, and agentLogHandlerObj. This improves test coverage and ensures consistent behavior during testing. * fix(agents): address review findings for summarization v2 Cancel rAF on unmount to prevent stale Recoil writes from dead component context. Clear orphaned summarizing:true parts when ON_SUMMARIZE_COMPLETE arrives without a summary payload. Add null guard and safe spread to agentLogHandler. Handle Anthropic-format base64 image/* documents in estimateMediaTokensForMessage. Use role="region" for expandable summary content. Add .describe() to contextMeta Zod fields. Extract duplicate usage loop into helper. * refactor: simplify contextMeta to calibrationRatio + encoding only Remove instructionOverhead and toolCount from cross-run persistence — instruction tokens change too frequently between runs (prompt edits, tool changes) for a persisted seed to be reliable. The intra-run calibration in the pruner still self-corrects via provider observations. contextMeta now stores only the tokenizer-bias ratio and encoding, which are stable across instruction changes. * test(SSE): enhance useStepHandler tests for ON_SUMMARIZE_COMPLETE behavior Updated the test for ON_SUMMARIZE_COMPLETE to clarify that it finalizes the existing part with summarizing set to false when the summary is undefined. Added assertions to verify the correct behavior of message updates and the state of summary parts. * refactor(BaseClient): remove handleContextStrategy and truncateToolCallOutputs functions Eliminated the handleContextStrategy method from BaseClient to streamline message handling. Also removed the truncateToolCallOutputs function from the prompts module, simplifying the codebase and improving maintainability. * refactor: add AGENT_DEBUG_LOGGING option and refactor token count handling in BaseClient Introduced AGENT_DEBUG_LOGGING to .env.example for enhanced debugging capabilities. Refactored token count handling in BaseClient by removing the handleTokenCountMap method and simplifying token count updates. Updated AgentClient to log detailed token count recalculations and adjustments, improving traceability during message processing. * chore: update dependencies in package-lock.json and package.json files Bumped versions of several dependencies, including @librechat/agents to ^3.1.62 and various AWS SDK packages to their latest versions. This ensures compatibility and incorporates the latest features and fixes. * chore: imports order * refactor: extract summarization config resolution from buildAgentContext * refactor: rename and simplify summarization configuration shaping function * refactor: replace AgentClient token counting methods with single-pass pure utility Extract getTokenCount() and getTokenCountForMessage() from AgentClient into countFormattedMessageTokens(), a pure function in packages/api that handles text, tool_call, image, and document content types in one loop. - Decompose estimateMediaTokensForMessage into block-level helpers (estimateImageDataTokens, estimateImageBlockTokens, estimateDocumentBlockTokens) shared by both estimateMediaTokensForMessage and the new single-pass function - Remove redundant per-call getEncoding() resolution (closure captures once) - Remove deprecated gpt-3.5-turbo-0301 model branching - Drop this.getTokenCount guard from BaseClient.sendMessage * refactor: streamline token counting in createTokenCounter function Simplified the createTokenCounter function by removing the media token estimation and directly calculating the token count. This change enhances clarity and performance by consolidating the token counting logic into a single pass, while maintaining compatibility with Claude's token correction. * refactor: simplify summarization configuration types Removed the AppSummarizationConfig type and directly used SummarizationConfig in the AppConfig interface. This change streamlines the type definitions and enhances consistency across the codebase. * chore: import order * fix: summarization event handling in useStepHandler - Cancel pending summarizeDeltaRaf in clearStepMaps to prevent stale frames firing after map reset or component unmount - Move announcePolite('summarize_completed') inside the didFinalize guard so screen readers only announce when finalization actually occurs - Remove dead cleanup closure returned from stepHandler useCallback body that was never invoked by any caller * fix: estimate tokens for non-PDF/non-image base64 document blocks Previously estimateDocumentBlockTokens returned 0 for unrecognized MIME types (e.g. text/plain, application/json), silently underestimating context budget. Fall back to character-based heuristic or countTokens. * refactor: return cloned usage from markSummarizationUsage Avoid mutating LangChain's internal usage_metadata object by returning a shallow clone with the usage_type tag. Update all call sites in callbacks, openai, and responses controllers to use the returned value. * refactor: consolidate debug logging loops in buildMessages Merge the two sequential O(n) debug-logging passes over orderedMessages into a single pass inside the map callback where all data is available. * refactor: narrow SummaryContentPart.content type Replace broad Agents.MessageContentComplex[] with the specific Array<{ type: ContentTypes.TEXT; text: string }> that all producers and consumers already use, improving compile-time safety. * refactor: use single output array in recordCollectedUsage Have processUsageGroup append to a shared array instead of returning separate arrays that are spread into a third, reducing allocations. * refactor: use for...in in hydrateMissingIndexTokenCounts Replace Object.entries with for...in to avoid allocating an intermediate tuple array during token map hydration. --- .env.example | 2 + api/app/clients/BaseClient.js | 398 +++--------- api/app/clients/prompts/truncate.js | 77 +-- api/app/clients/specs/BaseClient.test.js | 119 +++- api/package.json | 2 +- .../agents/__tests__/openai.spec.js | 12 +- .../agents/__tests__/responses.unit.spec.js | 48 +- api/server/controllers/agents/callbacks.js | 98 ++- api/server/controllers/agents/client.js | 185 +++--- api/server/controllers/agents/client.test.js | 2 +- api/server/controllers/agents/openai.js | 49 +- api/server/controllers/agents/responses.js | 38 +- api/server/controllers/assistants/helpers.js | 2 +- .../middleware/assistants/validateAuthor.js | 2 +- api/server/middleware/roles/index.js | 22 +- api/server/routes/admin/auth.js | 2 +- api/server/routes/files/files.agents.test.js | 3 +- api/server/routes/files/files.js | 2 +- api/server/routes/prompts.js | 2 +- api/server/routes/prompts.test.js | 1 - api/server/routes/roles.js | 3 +- api/server/services/ActionService.spec.js | 4 +- .../services/Endpoints/agents/initialize.js | 12 + api/server/services/Endpoints/index.js | 77 --- client/src/a11y/LiveAnnouncer.tsx | 3 + .../components/Chat/Messages/Content/Part.tsx | 20 +- .../Chat/Messages/Content/Parts/Summary.tsx | 327 ++++++++++ .../Chat/Messages/Content/Parts/index.ts | 1 + .../SSE/__tests__/useStepHandler.spec.ts | 542 ++++++++++++++-- client/src/hooks/SSE/useResumableSSE.ts | 3 +- client/src/hooks/SSE/useStepHandler.ts | 207 ++++-- client/src/locales/en/translation.json | 7 + package-lock.json | 346 +++++----- packages/api/package.json | 2 +- .../estimateMediaTokensForMessage.spec.ts | 282 +++++++++ .../src/agents/__tests__/initialize.test.ts | 58 +- .../__tests__/run-summarization.test.ts | 299 +++++++++ .../__tests__/summarization.e2e.test.ts | 595 ++++++++++++++++++ packages/api/src/agents/client.ts | 251 +++++++- packages/api/src/agents/initialize.ts | 31 +- packages/api/src/agents/run.ts | 94 ++- packages/api/src/agents/usage.spec.ts | 166 +++++ packages/api/src/agents/usage.ts | 171 ++--- packages/api/src/app/AppService.spec.ts | 37 ++ .../api/src/stream/interfaces/IJobStore.ts | 6 + packages/api/src/utils/index.ts | 1 + packages/api/src/utils/tokenMap.ts | 45 ++ packages/data-provider/src/config.ts | 31 + packages/data-provider/src/schemas.ts | 12 + packages/data-provider/src/types/agents.ts | 27 +- .../data-provider/src/types/assistants.ts | 16 + packages/data-provider/src/types/runs.ts | 14 + packages/data-schemas/src/app/service.ts | 33 +- packages/data-schemas/src/schema/message.ts | 8 + packages/data-schemas/src/types/app.ts | 3 + packages/data-schemas/src/types/message.ts | 4 + 56 files changed, 3822 insertions(+), 982 deletions(-) delete mode 100644 api/server/services/Endpoints/index.js create mode 100644 client/src/components/Chat/Messages/Content/Parts/Summary.tsx create mode 100644 packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts create mode 100644 packages/api/src/agents/__tests__/run-summarization.test.ts create mode 100644 packages/api/src/agents/__tests__/summarization.e2e.test.ts create mode 100644 packages/api/src/utils/tokenMap.ts diff --git a/.env.example b/.env.example index ae3537038a..db09bb471f 100644 --- a/.env.example +++ b/.env.example @@ -64,6 +64,8 @@ CONSOLE_JSON=false DEBUG_LOGGING=true DEBUG_CONSOLE=false +# Set to true to enable agent debug logging +AGENT_DEBUG_LOGGING=false # Enable memory diagnostics (logs heap/RSS snapshots every 60s, auto-enabled with --inspect) # MEM_DIAG=true diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index a7ad089d20..ae2d362773 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -13,7 +13,6 @@ const { } = require('@librechat/api'); const { Constants, - ErrorTypes, FileSources, ContentTypes, excludedKeys, @@ -25,7 +24,6 @@ const { isBedrockDocumentType, } = require('librechat-data-provider'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { truncateToolCallOutputs } = require('./prompts'); const { logViolation } = require('~/cache'); const TextStream = require('./TextStream'); const db = require('~/models'); @@ -333,45 +331,6 @@ class BaseClient { return payload; } - async handleTokenCountMap(tokenCountMap) { - if (this.clientName === EModelEndpoint.agents) { - return; - } - if (this.currentMessages.length === 0) { - return; - } - - for (let i = 0; i < this.currentMessages.length; i++) { - // Skip the last message, which is the user message. - if (i === this.currentMessages.length - 1) { - break; - } - - const message = this.currentMessages[i]; - const { messageId } = message; - const update = {}; - - if (messageId === tokenCountMap.summaryMessage?.messageId) { - logger.debug(`[BaseClient] Adding summary props to ${messageId}.`); - - update.summary = tokenCountMap.summaryMessage.content; - update.summaryTokenCount = tokenCountMap.summaryMessage.tokenCount; - } - - if (message.tokenCount && !update.summaryTokenCount) { - logger.debug(`[BaseClient] Skipping ${messageId}: already had a token count.`); - continue; - } - - const tokenCount = tokenCountMap[messageId]; - if (tokenCount) { - message.tokenCount = tokenCount; - update.tokenCount = tokenCount; - await this.updateMessageInDatabase({ messageId, ...update }); - } - } - } - concatenateMessages(messages) { return messages.reduce((acc, message) => { const nameOrRole = message.name ?? message.role; @@ -442,154 +401,6 @@ class BaseClient { }; } - async handleContextStrategy({ - instructions, - orderedMessages, - formattedMessages, - buildTokenMap = true, - }) { - let _instructions; - let tokenCount; - - if (instructions) { - ({ tokenCount, ..._instructions } = instructions); - } - - _instructions && logger.debug('[BaseClient] instructions tokenCount: ' + tokenCount); - if (tokenCount && tokenCount > this.maxContextTokens) { - const info = `${tokenCount} / ${this.maxContextTokens}`; - const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; - logger.warn(`Instructions token count exceeds max token count (${info}).`); - throw new Error(errorMessage); - } - - if (this.clientName === EModelEndpoint.agents) { - const { dbMessages, editedIndices } = truncateToolCallOutputs( - orderedMessages, - this.maxContextTokens, - this.getTokenCountForMessage.bind(this), - ); - - if (editedIndices.length > 0) { - logger.debug('[BaseClient] Truncated tool call outputs:', editedIndices); - for (const index of editedIndices) { - formattedMessages[index].content = dbMessages[index].content; - } - orderedMessages = dbMessages; - } - } - - let orderedWithInstructions = this.addInstructions(orderedMessages, instructions); - - let { context, remainingContextTokens, messagesToRefine } = - await this.getMessagesWithinTokenLimit({ - messages: orderedWithInstructions, - instructions, - }); - - logger.debug('[BaseClient] Context Count (1/2)', { - remainingContextTokens, - maxContextTokens: this.maxContextTokens, - }); - - let summaryMessage; - let summaryTokenCount; - let { shouldSummarize } = this; - - // Calculate the difference in length to determine how many messages were discarded if any - let payload; - let { length } = formattedMessages; - length += instructions != null ? 1 : 0; - const diff = length - context.length; - const firstMessage = orderedWithInstructions[0]; - const usePrevSummary = - shouldSummarize && - diff === 1 && - firstMessage?.summary && - this.previous_summary.messageId === firstMessage.messageId; - - if (diff > 0) { - payload = formattedMessages.slice(diff); - logger.debug( - `[BaseClient] Difference between original payload (${length}) and context (${context.length}): ${diff}`, - ); - } - - payload = this.addInstructions(payload ?? formattedMessages, _instructions); - - const latestMessage = orderedWithInstructions[orderedWithInstructions.length - 1]; - if (payload.length === 0 && !shouldSummarize && latestMessage) { - const info = `${latestMessage.tokenCount} / ${this.maxContextTokens}`; - const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; - logger.warn(`Prompt token count exceeds max token count (${info}).`); - throw new Error(errorMessage); - } else if ( - _instructions && - payload.length === 1 && - payload[0].content === _instructions.content - ) { - const info = `${tokenCount + 3} / ${this.maxContextTokens}`; - const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; - logger.warn( - `Including instructions, the prompt token count exceeds remaining max token count (${info}).`, - ); - throw new Error(errorMessage); - } - - if (usePrevSummary) { - summaryMessage = { role: 'system', content: firstMessage.summary }; - summaryTokenCount = firstMessage.summaryTokenCount; - payload.unshift(summaryMessage); - remainingContextTokens -= summaryTokenCount; - } else if (shouldSummarize && messagesToRefine.length > 0) { - ({ summaryMessage, summaryTokenCount } = await this.summarizeMessages({ - messagesToRefine, - remainingContextTokens, - })); - summaryMessage && payload.unshift(summaryMessage); - remainingContextTokens -= summaryTokenCount; - } - - // Make sure to only continue summarization logic if the summary message was generated - shouldSummarize = summaryMessage != null && shouldSummarize === true; - - logger.debug('[BaseClient] Context Count (2/2)', { - remainingContextTokens, - maxContextTokens: this.maxContextTokens, - }); - - /** @type {Record | undefined} */ - let tokenCountMap; - if (buildTokenMap) { - const currentPayload = shouldSummarize ? orderedWithInstructions : context; - tokenCountMap = currentPayload.reduce((map, message, index) => { - const { messageId } = message; - if (!messageId) { - return map; - } - - if (shouldSummarize && index === messagesToRefine.length - 1 && !usePrevSummary) { - map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount }; - } - - map[messageId] = currentPayload[index].tokenCount; - return map; - }, {}); - } - - const promptTokens = this.maxContextTokens - remainingContextTokens; - - logger.debug('[BaseClient] tokenCountMap:', tokenCountMap); - logger.debug('[BaseClient]', { - promptTokens, - remainingContextTokens, - payloadSize: payload.length, - maxContextTokens: this.maxContextTokens, - }); - - return { payload, tokenCountMap, promptTokens, messages: orderedWithInstructions }; - } - async sendMessage(message, opts = {}) { const appConfig = this.options.req?.config; /** @type {Promise} */ @@ -658,17 +469,13 @@ class BaseClient { opts, ); - if (tokenCountMap) { - if (tokenCountMap[userMessage.messageId]) { - userMessage.tokenCount = tokenCountMap[userMessage.messageId]; - logger.debug('[BaseClient] userMessage', { - messageId: userMessage.messageId, - tokenCount: userMessage.tokenCount, - conversationId: userMessage.conversationId, - }); - } - - this.handleTokenCountMap(tokenCountMap); + if (tokenCountMap && tokenCountMap[userMessage.messageId]) { + userMessage.tokenCount = tokenCountMap[userMessage.messageId]; + logger.debug('[BaseClient] userMessage', { + messageId: userMessage.messageId, + tokenCount: userMessage.tokenCount, + conversationId: userMessage.conversationId, + }); } if (!isEdited && !this.skipSaveUserMessage) { @@ -766,12 +573,7 @@ class BaseClient { responseMessage.text = completion.join(''); } - if ( - tokenCountMap && - this.recordTokenUsage && - this.getTokenCountForResponse && - this.getTokenCount - ) { + if (tokenCountMap && this.recordTokenUsage && this.getTokenCountForResponse) { let completionTokens; /** @@ -784,13 +586,6 @@ class BaseClient { if (usage != null && Number(usage[this.outputTokensKey]) > 0) { responseMessage.tokenCount = usage[this.outputTokensKey]; completionTokens = responseMessage.tokenCount; - await this.updateUserMessageTokenCount({ - usage, - tokenCountMap, - userMessage, - userMessagePromise, - opts, - }); } else { responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage); completionTokens = responseMessage.tokenCount; @@ -817,6 +612,27 @@ class BaseClient { await userMessagePromise; } + if ( + this.contextMeta?.calibrationRatio > 0 && + this.contextMeta.calibrationRatio !== 1 && + userMessage.tokenCount > 0 + ) { + const calibrated = Math.round(userMessage.tokenCount * this.contextMeta.calibrationRatio); + if (calibrated !== userMessage.tokenCount) { + logger.debug('[BaseClient] Calibrated user message tokenCount', { + messageId: userMessage.messageId, + raw: userMessage.tokenCount, + calibrated, + ratio: this.contextMeta.calibrationRatio, + }); + userMessage.tokenCount = calibrated; + await this.updateMessageInDatabase({ + messageId: userMessage.messageId, + tokenCount: calibrated, + }); + } + } + if (this.artifactPromises) { responseMessage.attachments = (await Promise.all(this.artifactPromises)).filter((a) => a); } @@ -829,6 +645,10 @@ class BaseClient { } } + if (this.contextMeta) { + responseMessage.contextMeta = this.contextMeta; + } + responseMessage.databasePromise = this.saveMessageToDatabase( responseMessage, saveOptions, @@ -839,75 +659,6 @@ class BaseClient { return responseMessage; } - /** - * Stream usage should only be used for user message token count re-calculation if: - * - The stream usage is available, with input tokens greater than 0, - * - the client provides a function to calculate the current token count, - * - files are being resent with every message (default behavior; or if `false`, with no attachments), - * - the `promptPrefix` (custom instructions) is not set. - * - * In these cases, the legacy token estimations would be more accurate. - * - * TODO: included system messages in the `orderedMessages` accounting, potentially as a - * separate message in the UI. ChatGPT does this through "hidden" system messages. - * @param {object} params - * @param {StreamUsage} params.usage - * @param {Record} params.tokenCountMap - * @param {TMessage} params.userMessage - * @param {Promise} params.userMessagePromise - * @param {object} params.opts - */ - async updateUserMessageTokenCount({ - usage, - tokenCountMap, - userMessage, - userMessagePromise, - opts, - }) { - /** @type {boolean} */ - const shouldUpdateCount = - this.calculateCurrentTokenCount != null && - Number(usage[this.inputTokensKey]) > 0 && - (this.options.resendFiles || - (!this.options.resendFiles && !this.options.attachments?.length)) && - !this.options.promptPrefix; - - if (!shouldUpdateCount) { - return; - } - - const userMessageTokenCount = this.calculateCurrentTokenCount({ - currentMessageId: userMessage.messageId, - tokenCountMap, - usage, - }); - - if (userMessageTokenCount === userMessage.tokenCount) { - return; - } - - userMessage.tokenCount = userMessageTokenCount; - /* - Note: `AgentController` saves the user message if not saved here - (noted by `savedMessageIds`), so we update the count of its `userMessage` reference - */ - if (typeof opts?.getReqData === 'function') { - opts.getReqData({ - userMessage, - }); - } - /* - Note: we update the user message to be sure it gets the calculated token count; - though `AgentController` saves the user message if not saved here - (noted by `savedMessageIds`), EditController does not - */ - await userMessagePromise; - await this.updateMessageInDatabase({ - messageId: userMessage.messageId, - tokenCount: userMessageTokenCount, - }); - } - async loadHistory(conversationId, parentMessageId = null) { logger.debug('[BaseClient] Loading history:', { conversationId, parentMessageId }); @@ -934,10 +685,24 @@ class BaseClient { return _messages; } - // Find the latest message with a 'summary' property for (let i = _messages.length - 1; i >= 0; i--) { - if (_messages[i]?.summary) { - this.previous_summary = _messages[i]; + const msg = _messages[i]; + if (!msg) { + continue; + } + + const summaryBlock = BaseClient.findSummaryContentBlock(msg); + if (summaryBlock) { + this.previous_summary = { + ...msg, + summary: BaseClient.getSummaryText(summaryBlock), + summaryTokenCount: summaryBlock.tokenCount, + }; + break; + } + + if (msg.summary) { + this.previous_summary = msg; break; } } @@ -1041,6 +806,34 @@ class BaseClient { await db.updateMessage(this.options?.req?.user?.id, message); } + /** Extracts text from a summary block (handles both legacy `text` field and new `content` array format). */ + static getSummaryText(summaryBlock) { + if (Array.isArray(summaryBlock.content)) { + return summaryBlock.content.map((b) => b.text ?? '').join(''); + } + if (typeof summaryBlock.content === 'string') { + return summaryBlock.content; + } + return summaryBlock.text ?? ''; + } + + /** Finds the last summary content block in a message's content array (last-summary-wins). */ + static findSummaryContentBlock(message) { + if (!Array.isArray(message?.content)) { + return null; + } + let lastSummary = null; + for (const part of message.content) { + if ( + part?.type === ContentTypes.SUMMARY && + BaseClient.getSummaryText(part).trim().length > 0 + ) { + lastSummary = part; + } + } + return lastSummary; + } + /** * Iterate through messages, building an array based on the parentMessageId. * @@ -1095,20 +888,35 @@ class BaseClient { break; } - if (summary && message.summary) { - message.role = 'system'; - message.text = message.summary; + let resolved = message; + let hasSummary = false; + if (summary) { + const summaryBlock = BaseClient.findSummaryContentBlock(message); + if (summaryBlock) { + const summaryText = BaseClient.getSummaryText(summaryBlock); + resolved = { + ...message, + role: 'system', + content: [{ type: ContentTypes.TEXT, text: summaryText }], + tokenCount: summaryBlock.tokenCount, + }; + hasSummary = true; + } else if (message.summary) { + resolved = { + ...message, + role: 'system', + content: [{ type: ContentTypes.TEXT, text: message.summary }], + tokenCount: message.summaryTokenCount ?? message.tokenCount, + }; + hasSummary = true; + } } - if (summary && message.summaryTokenCount) { - message.tokenCount = message.summaryTokenCount; - } - - const shouldMap = mapMethod != null && (mapCondition != null ? mapCondition(message) : true); - const processedMessage = shouldMap ? mapMethod(message) : message; + const shouldMap = mapMethod != null && (mapCondition != null ? mapCondition(resolved) : true); + const processedMessage = shouldMap ? mapMethod(resolved) : resolved; orderedMessages.push(processedMessage); - if (summary && message.summary) { + if (hasSummary) { break; } diff --git a/api/app/clients/prompts/truncate.js b/api/app/clients/prompts/truncate.js index 564b39efeb..e744b40daa 100644 --- a/api/app/clients/prompts/truncate.js +++ b/api/app/clients/prompts/truncate.js @@ -37,79 +37,4 @@ function smartTruncateText(text, maxLength = MAX_CHAR) { return text; } -/** - * @param {TMessage[]} _messages - * @param {number} maxContextTokens - * @param {function({role: string, content: TMessageContent[]}): number} getTokenCountForMessage - * - * @returns {{ - * dbMessages: TMessage[], - * editedIndices: number[] - * }} - */ -function truncateToolCallOutputs(_messages, maxContextTokens, getTokenCountForMessage) { - const THRESHOLD_PERCENTAGE = 0.5; - const targetTokenLimit = maxContextTokens * THRESHOLD_PERCENTAGE; - - let currentTokenCount = 3; - const messages = [..._messages]; - const processedMessages = []; - let currentIndex = messages.length; - const editedIndices = new Set(); - while (messages.length > 0) { - currentIndex--; - const message = messages.pop(); - currentTokenCount += message.tokenCount; - if (currentTokenCount < targetTokenLimit) { - processedMessages.push(message); - continue; - } - - if (!message.content || !Array.isArray(message.content)) { - processedMessages.push(message); - continue; - } - - const toolCallIndices = message.content - .map((item, index) => (item.type === 'tool_call' ? index : -1)) - .filter((index) => index !== -1) - .reverse(); - - if (toolCallIndices.length === 0) { - processedMessages.push(message); - continue; - } - - const newContent = [...message.content]; - - // Truncate all tool outputs since we're over threshold - for (const index of toolCallIndices) { - const toolCall = newContent[index].tool_call; - if (!toolCall || !toolCall.output) { - continue; - } - - editedIndices.add(currentIndex); - - newContent[index] = { - ...newContent[index], - tool_call: { - ...toolCall, - output: '[OUTPUT_OMITTED_FOR_BREVITY]', - }, - }; - } - - const truncatedMessage = { - ...message, - content: newContent, - tokenCount: getTokenCountForMessage({ role: 'assistant', content: newContent }), - }; - - processedMessages.push(truncatedMessage); - } - - return { dbMessages: processedMessages.reverse(), editedIndices: Array.from(editedIndices) }; -} - -module.exports = { truncateText, smartTruncateText, truncateToolCallOutputs }; +module.exports = { truncateText, smartTruncateText }; diff --git a/api/app/clients/specs/BaseClient.test.js b/api/app/clients/specs/BaseClient.test.js index f13c9979ac..edbbcaa87b 100644 --- a/api/app/clients/specs/BaseClient.test.js +++ b/api/app/clients/specs/BaseClient.test.js @@ -355,7 +355,8 @@ describe('BaseClient', () => { id: '3', parentMessageId: '2', role: 'system', - text: 'Summary for Message 3', + text: 'Message 3', + content: [{ type: 'text', text: 'Summary for Message 3' }], summary: 'Summary for Message 3', }, { id: '4', parentMessageId: '3', text: 'Message 4' }, @@ -380,7 +381,8 @@ describe('BaseClient', () => { id: '4', parentMessageId: '3', role: 'system', - text: 'Summary for Message 4', + text: 'Message 4', + content: [{ type: 'text', text: 'Summary for Message 4' }], summary: 'Summary for Message 4', }, { id: '5', parentMessageId: '4', text: 'Message 5' }, @@ -405,12 +407,123 @@ describe('BaseClient', () => { id: '4', parentMessageId: '3', role: 'system', - text: 'Summary for Message 4', + text: 'Message 4', + content: [{ type: 'text', text: 'Summary for Message 4' }], summary: 'Summary for Message 4', }, { id: '5', parentMessageId: '4', text: 'Message 5' }, ]); }); + + it('should detect summary content block and use it over legacy fields (summary mode)', () => { + const messagesWithContentBlock = [ + { id: '3', parentMessageId: '2', text: 'Message 3' }, + { + id: '2', + parentMessageId: '1', + text: 'Message 2', + content: [ + { type: 'text', text: 'Original text' }, + { type: 'summary', text: 'Content block summary', tokenCount: 42 }, + ], + }, + { id: '1', parentMessageId: null, text: 'Message 1' }, + ]; + const result = TestClient.constructor.getMessagesForConversation({ + messages: messagesWithContentBlock, + parentMessageId: '3', + summary: true, + }); + expect(result).toHaveLength(2); + expect(result[0].role).toBe('system'); + expect(result[0].content).toEqual([{ type: 'text', text: 'Content block summary' }]); + expect(result[0].tokenCount).toBe(42); + }); + + it('should prefer content block summary over legacy summary field', () => { + const messagesWithBoth = [ + { id: '2', parentMessageId: '1', text: 'Message 2' }, + { + id: '1', + parentMessageId: null, + text: 'Message 1', + summary: 'Legacy summary', + summaryTokenCount: 10, + content: [{ type: 'summary', text: 'Content block summary', tokenCount: 20 }], + }, + ]; + const result = TestClient.constructor.getMessagesForConversation({ + messages: messagesWithBoth, + parentMessageId: '2', + summary: true, + }); + expect(result).toHaveLength(2); + expect(result[0].content).toEqual([{ type: 'text', text: 'Content block summary' }]); + expect(result[0].tokenCount).toBe(20); + }); + + it('should fallback to legacy summary when no content block exists', () => { + const messagesWithLegacy = [ + { id: '2', parentMessageId: '1', text: 'Message 2' }, + { + id: '1', + parentMessageId: null, + text: 'Message 1', + summary: 'Legacy summary only', + summaryTokenCount: 15, + }, + ]; + const result = TestClient.constructor.getMessagesForConversation({ + messages: messagesWithLegacy, + parentMessageId: '2', + summary: true, + }); + expect(result).toHaveLength(2); + expect(result[0].content).toEqual([{ type: 'text', text: 'Legacy summary only' }]); + expect(result[0].tokenCount).toBe(15); + }); + }); + + describe('findSummaryContentBlock', () => { + it('should find a summary block in the content array', () => { + const message = { + content: [ + { type: 'text', text: 'some text' }, + { type: 'summary', text: 'Summary of conversation', tokenCount: 50 }, + ], + }; + const result = TestClient.constructor.findSummaryContentBlock(message); + expect(result).toBeTruthy(); + expect(result.text).toBe('Summary of conversation'); + expect(result.tokenCount).toBe(50); + }); + + it('should return null when no summary block exists', () => { + const message = { + content: [ + { type: 'text', text: 'some text' }, + { type: 'tool_call', tool_call: {} }, + ], + }; + expect(TestClient.constructor.findSummaryContentBlock(message)).toBeNull(); + }); + + it('should return null for string content', () => { + const message = { content: 'just a string' }; + expect(TestClient.constructor.findSummaryContentBlock(message)).toBeNull(); + }); + + it('should return null for missing content', () => { + expect(TestClient.constructor.findSummaryContentBlock({})).toBeNull(); + expect(TestClient.constructor.findSummaryContentBlock(null)).toBeNull(); + }); + + it('should skip summary blocks with no text', () => { + const message = { + content: [{ type: 'summary', tokenCount: 10 }], + }; + expect(TestClient.constructor.findSummaryContentBlock(message)).toBeNull(); + }); }); describe('sendMessage', () => { diff --git a/api/package.json b/api/package.json index aea98b3f8d..8b2f156cd3 100644 --- a/api/package.json +++ b/api/package.json @@ -44,7 +44,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.57", + "@librechat/agents": "^3.1.62", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/api/server/controllers/agents/__tests__/openai.spec.js b/api/server/controllers/agents/__tests__/openai.spec.js index deeb2ec51d..c2f13f7837 100644 --- a/api/server/controllers/agents/__tests__/openai.spec.js +++ b/api/server/controllers/agents/__tests__/openai.spec.js @@ -82,6 +82,9 @@ const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); jest.mock('~/server/controllers/agents/callbacks', () => ({ createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), + buildSummarizationHandlers: jest.fn().mockReturnValue({}), + markSummarizationUsage: jest.fn().mockImplementation((usage) => usage), + agentLogHandlerObj: { handle: jest.fn() }, })); jest.mock('~/server/services/PermissionService', () => ({ @@ -108,6 +111,7 @@ jest.mock('~/models', () => ({ getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier, getConvoFiles: jest.fn().mockResolvedValue([]), + getConvo: jest.fn().mockResolvedValue(null), })); describe('OpenAIChatCompletionController', () => { @@ -147,7 +151,7 @@ describe('OpenAIChatCompletionController', () => { describe('conversation ownership validation', () => { it('should skip ownership check when conversation_id is not provided', async () => { - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); await OpenAIChatCompletionController(req, res); expect(getConvo).not.toHaveBeenCalled(); }); @@ -164,7 +168,7 @@ describe('OpenAIChatCompletionController', () => { it('should return 404 when conversation is not owned by user', async () => { const { validateRequest } = require('@librechat/api'); - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); validateRequest.mockReturnValueOnce({ request: { model: 'agent-123', @@ -182,7 +186,7 @@ describe('OpenAIChatCompletionController', () => { it('should proceed when conversation is owned by user', async () => { const { validateRequest } = require('@librechat/api'); - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); validateRequest.mockReturnValueOnce({ request: { model: 'agent-123', @@ -200,7 +204,7 @@ describe('OpenAIChatCompletionController', () => { it('should return 500 when getConvo throws a DB error', async () => { const { validateRequest } = require('@librechat/api'); - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); validateRequest.mockReturnValueOnce({ request: { model: 'agent-123', diff --git a/api/server/controllers/agents/__tests__/responses.unit.spec.js b/api/server/controllers/agents/__tests__/responses.unit.spec.js index 0a63445f24..26f5f5d30b 100644 --- a/api/server/controllers/agents/__tests__/responses.unit.spec.js +++ b/api/server/controllers/agents/__tests__/responses.unit.spec.js @@ -104,10 +104,20 @@ jest.mock('~/server/services/ToolService', () => ({ const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); -jest.mock('~/server/controllers/agents/callbacks', () => ({ - createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), - createResponsesToolEndCallback: jest.fn().mockReturnValue(jest.fn()), -})); +jest.mock('~/server/controllers/agents/callbacks', () => { + const noop = { handle: jest.fn() }; + return { + createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), + createResponsesToolEndCallback: jest.fn().mockReturnValue(jest.fn()), + markSummarizationUsage: jest.fn().mockImplementation((usage) => usage), + agentLogHandlerObj: noop, + buildSummarizationHandlers: jest.fn().mockReturnValue({ + on_summarize_start: noop, + on_summarize_delta: noop, + on_summarize_complete: noop, + }), + }; +}); jest.mock('~/server/services/PermissionService', () => ({ findAccessibleResources: jest.fn().mockResolvedValue([]), @@ -175,7 +185,7 @@ describe('createResponse controller', () => { describe('conversation ownership validation', () => { it('should skip ownership check when previous_response_id is not provided', async () => { - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); await createResponse(req, res); expect(getConvo).not.toHaveBeenCalled(); }); @@ -202,7 +212,7 @@ describe('createResponse controller', () => { it('should return 404 when conversation is not owned by user', async () => { const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); validateResponseRequest.mockReturnValueOnce({ request: { model: 'agent-123', @@ -225,7 +235,7 @@ describe('createResponse controller', () => { it('should proceed when conversation is owned by user', async () => { const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); validateResponseRequest.mockReturnValueOnce({ request: { model: 'agent-123', @@ -248,7 +258,7 @@ describe('createResponse controller', () => { it('should return 500 when getConvo throws a DB error', async () => { const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); - const { getConvo } = require('~/models/Conversation'); + const { getConvo } = require('~/models'); validateResponseRequest.mockReturnValueOnce({ request: { model: 'agent-123', @@ -370,28 +380,7 @@ describe('createResponse controller', () => { it('should collect usage from on_chat_model_end events', async () => { const api = require('@librechat/api'); - let capturedOnChatModelEnd; - api.createAggregatorEventHandlers.mockImplementation(() => { - return { - on_message_delta: { handle: jest.fn() }, - on_reasoning_delta: { handle: jest.fn() }, - on_run_step: { handle: jest.fn() }, - on_run_step_delta: { handle: jest.fn() }, - on_chat_model_end: { - handle: jest.fn((event, data) => { - if (capturedOnChatModelEnd) { - capturedOnChatModelEnd(event, data); - } - }), - }, - }; - }); - api.createRun.mockImplementation(async ({ customHandlers }) => { - capturedOnChatModelEnd = (event, data) => { - customHandlers.on_chat_model_end.handle(event, data); - }; - return { processStream: jest.fn().mockImplementation(async () => { customHandlers.on_chat_model_end.handle('on_chat_model_end', { @@ -408,7 +397,6 @@ describe('createResponse controller', () => { }); await createResponse(req, res); - expect(mockRecordCollectedUsage).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index 0bb935795d..40fdf74212 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1,7 +1,13 @@ const { nanoid } = require('nanoid'); const { logger } = require('@librechat/data-schemas'); -const { Constants, EnvVar, GraphEvents, ToolEndHandler } = require('@librechat/agents'); const { Tools, StepTypes, FileContext, ErrorTypes } = require('librechat-data-provider'); +const { + EnvVar, + Constants, + GraphEvents, + GraphNodeKeys, + ToolEndHandler, +} = require('@librechat/agents'); const { sendEvent, GenerationJobManager, @@ -71,7 +77,9 @@ class ModelEndHandler { usage.model = modelName; } - this.collectedUsage.push(usage); + const taggedUsage = markSummarizationUsage(usage, metadata); + + this.collectedUsage.push(taggedUsage); } catch (error) { logger.error('Error handling model end event:', error); return this.finalize(errorMessage); @@ -133,6 +141,7 @@ function getDefaultHandlers({ collectedUsage, streamId = null, toolExecuteOptions = null, + summarizationOptions = null, }) { if (!res || !aggregateContent) { throw new Error( @@ -245,6 +254,37 @@ function getDefaultHandlers({ handlers[GraphEvents.ON_TOOL_EXECUTE] = createToolExecuteHandler(toolExecuteOptions); } + if (summarizationOptions?.enabled !== false) { + handlers[GraphEvents.ON_SUMMARIZE_START] = { + handle: async (_event, data) => { + await emitEvent(res, streamId, { + event: GraphEvents.ON_SUMMARIZE_START, + data, + }); + }, + }; + handlers[GraphEvents.ON_SUMMARIZE_DELTA] = { + handle: async (_event, data) => { + aggregateContent({ event: GraphEvents.ON_SUMMARIZE_DELTA, data }); + await emitEvent(res, streamId, { + event: GraphEvents.ON_SUMMARIZE_DELTA, + data, + }); + }, + }; + handlers[GraphEvents.ON_SUMMARIZE_COMPLETE] = { + handle: async (_event, data) => { + aggregateContent({ event: GraphEvents.ON_SUMMARIZE_COMPLETE, data }); + await emitEvent(res, streamId, { + event: GraphEvents.ON_SUMMARIZE_COMPLETE, + data, + }); + }, + }; + } + + handlers[GraphEvents.ON_AGENT_LOG] = { handle: agentLogHandler }; + return handlers; } @@ -668,8 +708,62 @@ function createResponsesToolEndCallback({ req, res, tracker, artifactPromises }) }; } +const ALLOWED_LOG_LEVELS = new Set(['debug', 'info', 'warn', 'error']); + +function agentLogHandler(_event, data) { + if (!data) { + return; + } + const logFn = ALLOWED_LOG_LEVELS.has(data.level) ? logger[data.level] : logger.debug; + const meta = typeof data.data === 'object' && data.data != null ? data.data : {}; + logFn(`[agents:${data.scope ?? 'unknown'}] ${data.message ?? ''}`, { + ...meta, + runId: data.runId, + agentId: data.agentId, + }); +} + +function markSummarizationUsage(usage, metadata) { + const node = metadata?.langgraph_node; + if (typeof node === 'string' && node.startsWith(GraphNodeKeys.SUMMARIZE)) { + return { ...usage, usage_type: 'summarization' }; + } + return usage; +} + +const agentLogHandlerObj = { handle: agentLogHandler }; + +/** + * Builds the three summarization SSE event handlers. + * In streaming mode, each event is forwarded to the client via `res.write`. + * In non-streaming mode, the handlers are no-ops. + * @param {{ isStreaming: boolean, res: import('express').Response }} opts + */ +function buildSummarizationHandlers({ isStreaming, res }) { + if (!isStreaming) { + const noop = { handle: () => {} }; + return { on_summarize_start: noop, on_summarize_delta: noop, on_summarize_complete: noop }; + } + const writeEvent = (name) => ({ + handle: async (_event, data) => { + if (!res.writableEnded) { + res.write(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`); + } + }, + }); + return { + on_summarize_start: writeEvent('on_summarize_start'), + on_summarize_delta: writeEvent('on_summarize_delta'), + on_summarize_complete: writeEvent('on_summarize_complete'), + }; +} + module.exports = { + agentLogHandler, + agentLogHandlerObj, getDefaultHandlers, createToolEndCallback, + markSummarizationUsage, + buildSummarizationHandlers, createResponsesToolEndCallback, }; diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index bf75838a87..47a10165e3 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -3,11 +3,11 @@ const { logger } = require('@librechat/data-schemas'); const { getBufferString, HumanMessage } = require('@langchain/core/messages'); const { createRun, - Tokenizer, + isEnabled, checkAccess, buildToolSet, - sanitizeTitle, logToolError, + sanitizeTitle, payloadParser, resolveHeaders, createSafeUser, @@ -25,6 +25,8 @@ const { loadAgent: loadAgentFn, createMultiAgentMapper, filterMalformedContentParts, + countFormattedMessageTokens, + hydrateMissingIndexTokenCounts, } = require('@librechat/api'); const { Callback, @@ -62,9 +64,6 @@ class AgentClient extends BaseClient { * @type {string} */ this.clientName = EModelEndpoint.agents; - /** @type {'discard' | 'summarize'} */ - this.contextStrategy = 'discard'; - /** @deprecated @type {true} - Is a Chat Completion Request */ this.isChatCompletion = true; @@ -216,7 +215,6 @@ class AgentClient extends BaseClient { })) : []), ]; - if (this.options.attachments) { const attachments = await this.options.attachments; const latestMessage = orderedMessages[orderedMessages.length - 1]; @@ -243,6 +241,11 @@ class AgentClient extends BaseClient { ); } + /** @type {Record} */ + const canonicalTokenCountMap = {}; + /** @type {Record} */ + const tokenCountMap = {}; + let promptTokenTotal = 0; const formattedMessages = orderedMessages.map((message, i) => { const formattedMessage = formatMessage({ message, @@ -262,12 +265,14 @@ class AgentClient extends BaseClient { } } - const needsTokenCount = - (this.contextStrategy && !orderedMessages[i].tokenCount) || message.fileContext; + const dbTokenCount = orderedMessages[i].tokenCount; + const needsTokenCount = !dbTokenCount || message.fileContext; - /* If tokens were never counted, or, is a Vision request and the message has files, count again */ if (needsTokenCount || (this.isVisionModel && (message.image_urls || message.files))) { - orderedMessages[i].tokenCount = this.getTokenCountForMessage(formattedMessage); + orderedMessages[i].tokenCount = countFormattedMessageTokens( + formattedMessage, + this.getEncoding(), + ); } /* If message has files, calculate image token cost */ @@ -281,17 +286,37 @@ class AgentClient extends BaseClient { if (file.metadata?.fileIdentifier) { continue; } - // orderedMessages[i].tokenCount += this.calculateImageTokenCost({ - // width: file.width, - // height: file.height, - // detail: this.options.imageDetail ?? ImageDetail.auto, - // }); } } + const tokenCount = Number(orderedMessages[i].tokenCount); + const normalizedTokenCount = Number.isFinite(tokenCount) && tokenCount > 0 ? tokenCount : 0; + canonicalTokenCountMap[i] = normalizedTokenCount; + promptTokenTotal += normalizedTokenCount; + + if (message.messageId) { + tokenCountMap[message.messageId] = normalizedTokenCount; + } + + if (isEnabled(process.env.AGENT_DEBUG_LOGGING)) { + const role = message.isCreatedByUser ? 'user' : 'assistant'; + const hasSummary = + Array.isArray(message.content) && message.content.some((p) => p && p.type === 'summary'); + const suffix = hasSummary ? '[S]' : ''; + const id = (message.messageId ?? message.id ?? '').slice(-8); + const recalced = needsTokenCount ? orderedMessages[i].tokenCount : null; + logger.debug( + `[AgentClient] msg[${i}] ${role}${suffix} id=…${id} db=${dbTokenCount} needsRecount=${needsTokenCount} recalced=${recalced} tokens=${normalizedTokenCount}`, + ); + } + return formattedMessage; }); + payload = formattedMessages; + messages = orderedMessages; + promptTokens = promptTokenTotal; + /** * Build shared run context - applies to ALL agents in the run. * This includes: file context (latest message), augmented prompt (RAG), memory context. @@ -321,23 +346,20 @@ class AgentClient extends BaseClient { const sharedRunContext = sharedRunContextParts.join('\n\n'); - /** @type {Record | undefined} */ - let tokenCountMap; + /** Preserve canonical pre-format token counts for all history entering graph formatting */ + this.indexTokenCountMap = canonicalTokenCountMap; - if (this.contextStrategy) { - ({ payload, promptTokens, tokenCountMap, messages } = await this.handleContextStrategy({ - orderedMessages, - formattedMessages, - })); - } - - for (let i = 0; i < messages.length; i++) { - this.indexTokenCountMap[i] = messages[i].tokenCount; + /** Extract contextMeta from the parent response (second-to-last in ordered chain; + * last is the current user message). Seeds the pruner's calibration EMA for this run. */ + const parentResponse = + orderedMessages.length >= 2 ? orderedMessages[orderedMessages.length - 2] : undefined; + if (parentResponse?.contextMeta && !parentResponse.isCreatedByUser) { + this.contextMeta = parentResponse.contextMeta; } const result = { - tokenCountMap, prompt: payload, + tokenCountMap, promptTokens, messages, }; @@ -665,39 +687,7 @@ class AgentClient extends BaseClient { * @returns {number} */ getTokenCountForResponse({ content }) { - return this.getTokenCountForMessage({ - role: 'assistant', - content, - }); - } - - /** - * Calculates the correct token count for the current user message based on the token count map and API usage. - * Edge case: If the calculation results in a negative value, it returns the original estimate. - * If revisiting a conversation with a chat history entirely composed of token estimates, - * the cumulative token count going forward should become more accurate as the conversation progresses. - * @param {Object} params - The parameters for the calculation. - * @param {Record} params.tokenCountMap - A map of message IDs to their token counts. - * @param {string} params.currentMessageId - The ID of the current message to calculate. - * @param {OpenAIUsageMetadata} params.usage - The usage object returned by the API. - * @returns {number} The correct token count for the current user message. - */ - calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage }) { - const originalEstimate = tokenCountMap[currentMessageId] || 0; - - if (!usage || typeof usage[this.inputTokensKey] !== 'number') { - return originalEstimate; - } - - tokenCountMap[currentMessageId] = 0; - const totalTokensFromMap = Object.values(tokenCountMap).reduce((sum, count) => { - const numCount = Number(count); - return sum + (isNaN(numCount) ? 0 : numCount); - }, 0); - const totalInputTokens = usage[this.inputTokensKey] ?? 0; - - const currentMessageTokens = totalInputTokens - totalTokensFromMap; - return currentMessageTokens > 0 ? currentMessageTokens : originalEstimate; + return countFormattedMessageTokens({ role: 'assistant', content }, this.getEncoding()); } /** @@ -745,11 +735,34 @@ class AgentClient extends BaseClient { }; const toolSet = buildToolSet(this.options.agent); - let { messages: initialMessages, indexTokenCountMap } = formatAgentMessages( - payload, - this.indexTokenCountMap, - toolSet, - ); + const tokenCounter = createTokenCounter(this.getEncoding()); + let { + messages: initialMessages, + indexTokenCountMap, + summary: initialSummary, + boundaryTokenAdjustment, + } = formatAgentMessages(payload, this.indexTokenCountMap, toolSet); + if (boundaryTokenAdjustment) { + logger.debug( + `[AgentClient] Boundary token adjustment: ${boundaryTokenAdjustment.original} → ${boundaryTokenAdjustment.adjusted} (${boundaryTokenAdjustment.remainingChars}/${boundaryTokenAdjustment.totalChars} chars)`, + ); + } + if (indexTokenCountMap && isEnabled(process.env.AGENT_DEBUG_LOGGING)) { + const entries = Object.entries(indexTokenCountMap); + const perMsg = entries.map(([idx, count]) => { + const msg = initialMessages[Number(idx)]; + const type = msg ? msg._getType() : '?'; + return `${idx}:${type}=${count}`; + }); + logger.debug( + `[AgentClient] Token map after format: [${perMsg.join(', ')}] (payload=${payload.length}, formatted=${initialMessages.length})`, + ); + } + indexTokenCountMap = hydrateMissingIndexTokenCounts({ + messages: initialMessages, + indexTokenCountMap, + tokenCounter, + }); /** * @param {BaseMessage[]} messages @@ -803,16 +816,32 @@ class AgentClient extends BaseClient { memoryPromise = this.runMemory(messages); + /** Seed calibration state from previous run if encoding matches */ + const currentEncoding = this.getEncoding(); + const prevMeta = this.contextMeta; + const encodingMatch = prevMeta?.encoding === currentEncoding; + const calibrationRatio = + encodingMatch && prevMeta?.calibrationRatio > 0 ? prevMeta.calibrationRatio : undefined; + + if (prevMeta) { + logger.debug( + `[AgentClient] contextMeta from parent: ratio=${prevMeta.calibrationRatio}, encoding=${prevMeta.encoding}, current=${currentEncoding}, seeded=${calibrationRatio ?? 'none'}`, + ); + } + run = await createRun({ agents, messages, indexTokenCountMap, + initialSummary, + calibrationRatio, runId: this.responseMessageId, signal: abortController.signal, customHandlers: this.options.eventHandlers, requestBody: config.configurable.requestBody, user: createSafeUser(this.options.req?.user), - tokenCounter: createTokenCounter(this.getEncoding()), + summarizationConfig: appConfig?.summarization, + tokenCounter, }); if (!run) { @@ -843,6 +872,7 @@ class AgentClient extends BaseClient { const hideSequentialOutputs = config.configurable.hide_sequential_outputs; await runAgents(initialMessages); + /** @deprecated Agent Chain */ if (hideSequentialOutputs) { this.contentParts = this.contentParts.filter((part, index) => { @@ -873,6 +903,18 @@ class AgentClient extends BaseClient { }); } } finally { + /** Capture calibration state from the run for persistence on the response message. + * Runs in finally so values are captured even on abort. */ + const ratio = this.run?.getCalibrationRatio() ?? 0; + if (ratio > 0 && ratio !== 1) { + this.contextMeta = { + calibrationRatio: Math.round(ratio * 1000) / 1000, + encoding: this.getEncoding(), + }; + } else { + this.contextMeta = undefined; + } + try { const attachments = await this.awaitMemoryWithTimeout(memoryPromise); if (attachments && attachments.length > 0) { @@ -1058,6 +1100,7 @@ class AgentClient extends BaseClient { titlePrompt: endpointConfig?.titlePrompt, titlePromptTemplate: endpointConfig?.titlePromptTemplate, chainOptions: { + runName: 'TitleRun', signal: abortController.signal, callbacks: [ { @@ -1179,16 +1222,6 @@ class AgentClient extends BaseClient { } return 'o200k_base'; } - - /** - * Returns the token count of a given text. It also checks and resets the tokenizers if necessary. - * @param {string} text - The text to get the token count for. - * @returns {number} The token count of the given text. - */ - getTokenCount(text) { - const encoding = this.getEncoding(); - return Tokenizer.getTokenCount(text, encoding); - } } module.exports = AgentClient; diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 4e3d10e8e6..41a806f66d 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -1818,7 +1818,7 @@ describe('AgentClient - titleConvo', () => { /** Traversal stops at msg-2 (has summary), so we get msg-4 -> msg-3 -> msg-2 */ expect(result).toHaveLength(3); - expect(result[0].text).toBe('Summary of conversation'); + expect(result[0].content).toEqual([{ type: 'text', text: 'Summary of conversation' }]); expect(result[0].role).toBe('system'); expect(result[0].mapped).toBe(true); expect(result[1].mapped).toBe(true); diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index ae2e462103..b649058806 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -21,8 +21,13 @@ const { createOpenAIContentAggregator, isChatCompletionValidationFailure, } = require('@librechat/api'); +const { + buildSummarizationHandlers, + markSummarizationUsage, + createToolEndCallback, + agentLogHandlerObj, +} = require('~/server/controllers/agents/callbacks'); const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); -const { createToolEndCallback } = require('~/server/controllers/agents/callbacks'); const { findAccessibleResources } = require('~/server/services/PermissionService'); const db = require('~/models'); @@ -181,7 +186,7 @@ const OpenAIChatCompletionController = async (req, res) => { 'invalid_request_error', ); } - if (!(await getConvo(req.user?.id, request.conversation_id))) { + if (!(await db.getConvo(req.user?.id, request.conversation_id))) { return sendErrorResponse(res, 404, 'Conversation not found', 'invalid_request_error'); } } @@ -282,14 +287,16 @@ const OpenAIChatCompletionController = async (req, res) => { toolEndCallback, }; + const summarizationConfig = appConfig?.summarization; + const openaiMessages = convertMessages(request.messages); const toolSet = buildToolSet(primaryConfig); - const { messages: formattedMessages, indexTokenCountMap } = formatAgentMessages( - openaiMessages, - {}, - toolSet, - ); + const { + messages: formattedMessages, + indexTokenCountMap, + summary: initialSummary, + } = formatAgentMessages(openaiMessages, {}, toolSet); /** * Create a simple handler that processes data @@ -432,24 +439,30 @@ const OpenAIChatCompletionController = async (req, res) => { }), // Usage tracking - on_chat_model_end: createHandler((data) => { - const usage = data?.output?.usage_metadata; - if (usage) { - collectedUsage.push(usage); - const target = isStreaming ? tracker : aggregator; - target.usage.promptTokens += usage.input_tokens ?? 0; - target.usage.completionTokens += usage.output_tokens ?? 0; - } - }), + on_chat_model_end: { + handle: (_event, data, metadata) => { + const usage = data?.output?.usage_metadata; + if (usage) { + const taggedUsage = markSummarizationUsage(usage, metadata); + collectedUsage.push(taggedUsage); + const target = isStreaming ? tracker : aggregator; + target.usage.promptTokens += taggedUsage.input_tokens ?? 0; + target.usage.completionTokens += taggedUsage.output_tokens ?? 0; + } + }, + }, on_run_step_completed: createHandler(), // Use proper ToolEndHandler for processing artifacts (images, file citations, code output) on_tool_end: new ToolEndHandler(toolEndCallback, logger), on_chain_stream: createHandler(), on_chain_end: createHandler(), on_agent_update: createHandler(), + on_agent_log: agentLogHandlerObj, on_custom_event: createHandler(), - // Event-driven tool execution handler on_tool_execute: createToolExecuteHandler(toolExecuteOptions), + ...(summarizationConfig?.enabled !== false + ? buildSummarizationHandlers({ isStreaming, res }) + : {}), }; // Create and run the agent @@ -462,7 +475,9 @@ const OpenAIChatCompletionController = async (req, res) => { agents: [primaryConfig], messages: formattedMessages, indexTokenCountMap, + initialSummary, runId: responseId, + summarizationConfig, signal: abortController.signal, customHandlers: handlers, requestBody: { diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index 62cedb14fd..7abddf5e2f 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -32,7 +32,10 @@ const { } = require('@librechat/api'); const { createResponsesToolEndCallback, + buildSummarizationHandlers, + markSummarizationUsage, createToolEndCallback, + agentLogHandlerObj, } = require('~/server/controllers/agents/callbacks'); const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); const { findAccessibleResources } = require('~/server/services/PermissionService'); @@ -277,6 +280,7 @@ const createResponse = async (req, res) => { const request = validation.request; const agentId = request.model; const isStreaming = request.stream === true; + const summarizationConfig = req.config?.summarization; // Look up the agent const agent = await db.getAgent({ id: agentId }); @@ -319,7 +323,7 @@ const createResponse = async (req, res) => { 'invalid_request', ); } - if (!(await getConvo(req.user?.id, request.previous_response_id))) { + if (!(await db.getConvo(req.user?.id, request.previous_response_id))) { return sendResponsesErrorResponse(res, 404, 'Conversation not found', 'not_found'); } } @@ -387,11 +391,11 @@ const createResponse = async (req, res) => { const allMessages = [...previousMessages, ...inputMessages]; const toolSet = buildToolSet(primaryConfig); - const { messages: formattedMessages, indexTokenCountMap } = formatAgentMessages( - allMessages, - {}, - toolSet, - ); + const { + messages: formattedMessages, + indexTokenCountMap, + summary: initialSummary, + } = formatAgentMessages(allMessages, {}, toolSet); // Create tracker for streaming or aggregator for non-streaming const tracker = actuallyStreaming ? createResponseTracker() : null; @@ -455,11 +459,12 @@ const createResponse = async (req, res) => { on_run_step: responsesHandlers.on_run_step, on_run_step_delta: responsesHandlers.on_run_step_delta, on_chat_model_end: { - handle: (event, data) => { + handle: (event, data, metadata) => { responsesHandlers.on_chat_model_end.handle(event, data); const usage = data?.output?.usage_metadata; if (usage) { - collectedUsage.push(usage); + const taggedUsage = markSummarizationUsage(usage, metadata); + collectedUsage.push(taggedUsage); } }, }, @@ -470,6 +475,10 @@ const createResponse = async (req, res) => { on_agent_update: { handle: () => {} }, on_custom_event: { handle: () => {} }, on_tool_execute: createToolExecuteHandler(toolExecuteOptions), + on_agent_log: agentLogHandlerObj, + ...(summarizationConfig?.enabled !== false + ? buildSummarizationHandlers({ isStreaming: actuallyStreaming, res }) + : {}), }; // Create and run the agent @@ -480,7 +489,9 @@ const createResponse = async (req, res) => { agents: [primaryConfig], messages: formattedMessages, indexTokenCountMap, + initialSummary, runId: responseId, + summarizationConfig, signal: abortController.signal, customHandlers: handlers, requestBody: { @@ -612,11 +623,12 @@ const createResponse = async (req, res) => { on_run_step: aggregatorHandlers.on_run_step, on_run_step_delta: aggregatorHandlers.on_run_step_delta, on_chat_model_end: { - handle: (event, data) => { + handle: (event, data, metadata) => { aggregatorHandlers.on_chat_model_end.handle(event, data); const usage = data?.output?.usage_metadata; if (usage) { - collectedUsage.push(usage); + const taggedUsage = markSummarizationUsage(usage, metadata); + collectedUsage.push(taggedUsage); } }, }, @@ -627,6 +639,10 @@ const createResponse = async (req, res) => { on_agent_update: { handle: () => {} }, on_custom_event: { handle: () => {} }, on_tool_execute: createToolExecuteHandler(toolExecuteOptions), + on_agent_log: agentLogHandlerObj, + ...(summarizationConfig?.enabled !== false + ? buildSummarizationHandlers({ isStreaming: false, res }) + : {}), }; const userId = req.user?.id ?? 'api-user'; @@ -636,7 +652,9 @@ const createResponse = async (req, res) => { agents: [primaryConfig], messages: formattedMessages, indexTokenCountMap, + initialSummary, runId: responseId, + summarizationConfig, signal: abortController.signal, customHandlers: handlers, requestBody: { diff --git a/api/server/controllers/assistants/helpers.js b/api/server/controllers/assistants/helpers.js index 6309268770..4630bfe7ef 100644 --- a/api/server/controllers/assistants/helpers.js +++ b/api/server/controllers/assistants/helpers.js @@ -8,8 +8,8 @@ const { initializeClient: initAzureClient, } = require('~/server/services/Endpoints/azureAssistants'); const { initializeClient } = require('~/server/services/Endpoints/assistants'); +const { hasCapability } = require('~/server/middleware/roles/capabilities'); const { getEndpointsConfig } = require('~/server/services/Config'); -const { hasCapability } = require('~/server/middleware'); /** * @param {ServerRequest} req diff --git a/api/server/middleware/assistants/validateAuthor.js b/api/server/middleware/assistants/validateAuthor.js index 3be1642a71..024d6abbe3 100644 --- a/api/server/middleware/assistants/validateAuthor.js +++ b/api/server/middleware/assistants/validateAuthor.js @@ -1,5 +1,5 @@ const { logger, SystemCapabilities } = require('@librechat/data-schemas'); -const { hasCapability } = require('~/server/middleware'); +const { hasCapability } = require('~/server/middleware/roles/capabilities'); const { getAssistant } = require('~/models'); /** diff --git a/api/server/middleware/roles/index.js b/api/server/middleware/roles/index.js index e6c315d007..f97d4b72b4 100644 --- a/api/server/middleware/roles/index.js +++ b/api/server/middleware/roles/index.js @@ -1,15 +1,17 @@ -const { - hasCapability, - requireCapability, - hasConfigCapability, - capabilityContextMiddleware, -} = require('./capabilities'); +/** + * NOTE: hasCapability, requireCapability, hasConfigCapability, and + * capabilityContextMiddleware are intentionally NOT re-exported here. + * + * capabilities.js depends on ~/models, and the middleware barrel + * (middleware/index.js) is frequently required by modules that are + * themselves loaded while the barrel is still initialising — creating + * a circular-require that silently returns an empty exports object. + * + * Always import capability helpers directly: + * require('~/server/middleware/roles/capabilities') + */ const checkAdmin = require('./admin'); module.exports = { checkAdmin, - hasCapability, - requireCapability, - hasConfigCapability, - capabilityContextMiddleware, }; diff --git a/api/server/routes/admin/auth.js b/api/server/routes/admin/auth.js index e19adf54a9..530764852b 100644 --- a/api/server/routes/admin/auth.js +++ b/api/server/routes/admin/auth.js @@ -6,10 +6,10 @@ const { CacheKeys } = require('librechat-data-provider'); const { SystemCapabilities } = require('@librechat/data-schemas'); const { getAdminPanelUrl, exchangeAdminCode, createSetBalanceConfig } = require('@librechat/api'); const { loginController } = require('~/server/controllers/auth/LoginController'); +const { requireCapability } = require('~/server/middleware/roles/capabilities'); const { createOAuthHandler } = require('~/server/controllers/auth/oauth'); const { findBalanceByUser, upsertBalanceFields } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); -const { requireCapability } = require('~/server/middleware'); const getLogStores = require('~/cache/getLogStores'); const { getOpenIdConfig } = require('~/strategies'); const middleware = require('~/server/middleware'); diff --git a/api/server/routes/files/files.agents.test.js b/api/server/routes/files/files.agents.test.js index 5a01df022d..cb0e4ff3d2 100644 --- a/api/server/routes/files/files.agents.test.js +++ b/api/server/routes/files/files.agents.test.js @@ -2,15 +2,14 @@ const express = require('express'); const request = require('supertest'); const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); -const { createMethods } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const { createMethods, SystemCapabilities } = require('@librechat/data-schemas'); const { SystemRoles, AccessRoleIds, ResourceType, PrincipalType, } = require('librechat-data-provider'); -const { SystemCapabilities } = require('@librechat/data-schemas'); const { createAgent, createFile } = require('~/models'); // Only mock the external dependencies that we don't want to test diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index e1b420fb5d..eb13ecdc31 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -27,11 +27,11 @@ const { const { fileAccess } = require('~/server/middleware/accessResources/fileAccess'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); +const { hasCapability } = require('~/server/middleware/roles/capabilities'); const { checkPermission } = require('~/server/services/PermissionService'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { hasAccessToFilesViaAgent } = require('~/server/services/Files'); const { cleanFileName } = require('~/server/utils/files'); -const { hasCapability } = require('~/server/middleware'); const { getLogStores } = require('~/cache'); const { Readable } = require('stream'); const db = require('~/models'); diff --git a/api/server/routes/prompts.js b/api/server/routes/prompts.js index c2e15ac6c0..60165d367b 100644 --- a/api/server/routes/prompts.js +++ b/api/server/routes/prompts.js @@ -32,7 +32,6 @@ const { getPrompt, } = require('~/models'); const { - hasCapability, canAccessPromptGroupResource, canAccessPromptViaGroup, requireJwtAuth, @@ -43,6 +42,7 @@ const { findAccessibleResources, grantPermission, } = require('~/server/services/PermissionService'); +const { hasCapability } = require('~/server/middleware/roles/capabilities'); const router = express.Router(); diff --git a/api/server/routes/prompts.test.js b/api/server/routes/prompts.test.js index ec162ac1fb..a3b868f022 100644 --- a/api/server/routes/prompts.test.js +++ b/api/server/routes/prompts.test.js @@ -36,7 +36,6 @@ jest.mock('~/models', () => { jest.mock('~/server/middleware', () => ({ requireJwtAuth: (req, res, next) => next(), - hasCapability: jest.requireActual('~/server/middleware').hasCapability, canAccessPromptViaGroup: jest.requireActual('~/server/middleware').canAccessPromptViaGroup, canAccessPromptGroupResource: jest.requireActual('~/server/middleware').canAccessPromptGroupResource, diff --git a/api/server/routes/roles.js b/api/server/routes/roles.js index 1b7e4632e3..25ee47854d 100644 --- a/api/server/routes/roles.js +++ b/api/server/routes/roles.js @@ -12,8 +12,9 @@ const { peoplePickerPermissionsSchema, remoteAgentsPermissionsSchema, } = require('librechat-data-provider'); -const { hasCapability, requireCapability, requireJwtAuth } = require('~/server/middleware'); +const { hasCapability, requireCapability } = require('~/server/middleware/roles/capabilities'); const { updateRoleByName, getRoleByName } = require('~/models'); +const { requireJwtAuth } = require('~/server/middleware'); const router = express.Router(); router.use(requireJwtAuth); diff --git a/api/server/services/ActionService.spec.js b/api/server/services/ActionService.spec.js index 42def44b4f..52419975f7 100644 --- a/api/server/services/ActionService.spec.js +++ b/api/server/services/ActionService.spec.js @@ -3,12 +3,12 @@ const { domainParser, legacyDomainEncode, validateAndUpdateTool } = require('./A jest.mock('keyv'); -jest.mock('~/models/Action', () => ({ +jest.mock('~/models', () => ({ getActions: jest.fn(), deleteActions: jest.fn(), })); -const { getActions } = require('~/models/Action'); +const { getActions } = require('~/models'); let mockDomainCache = {}; jest.mock('~/cache/getLogStores', () => { diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 28282e68ea..69767e191c 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -82,6 +82,14 @@ function createToolLoader(signal, streamId = null, definitionsOnly = false) { }; } +/** + * Initializes the AgentClient for a given request/response cycle. + * @param {Object} params + * @param {Express.Request} params.req + * @param {Express.Response} params.res + * @param {AbortSignal} params.signal + * @param {Object} params.endpointOption + */ const initializeClient = async ({ req, res, signal, endpointOption }) => { if (!endpointOption) { throw new Error('Endpoint option not provided'); @@ -136,9 +144,13 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { toolEndCallback, }; + const summarizationOptions = + appConfig?.summarization?.enabled === false ? { enabled: false } : { enabled: true }; + const eventHandlers = getDefaultHandlers({ res, toolExecuteOptions, + summarizationOptions, aggregateContent, toolEndCallback, collectedUsage, diff --git a/api/server/services/Endpoints/index.js b/api/server/services/Endpoints/index.js deleted file mode 100644 index 3cabfe1c58..0000000000 --- a/api/server/services/Endpoints/index.js +++ /dev/null @@ -1,77 +0,0 @@ -const { Providers } = require('@librechat/agents'); -const { EModelEndpoint } = require('librechat-data-provider'); -const { getCustomEndpointConfig } = require('@librechat/api'); -const initAnthropic = require('~/server/services/Endpoints/anthropic/initialize'); -const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options'); -const initOpenAI = require('~/server/services/Endpoints/openAI/initialize'); -const initCustom = require('~/server/services/Endpoints/custom/initialize'); -const initGoogle = require('~/server/services/Endpoints/google/initialize'); - -/** Check if the provider is a known custom provider - * @param {string | undefined} [provider] - The provider string - * @returns {boolean} - True if the provider is a known custom provider, false otherwise - */ -function isKnownCustomProvider(provider) { - return [Providers.XAI, Providers.DEEPSEEK, Providers.OPENROUTER, Providers.MOONSHOT].includes( - provider?.toLowerCase() || '', - ); -} - -const providerConfigMap = { - [Providers.XAI]: initCustom, - [Providers.DEEPSEEK]: initCustom, - [Providers.MOONSHOT]: initCustom, - [Providers.OPENROUTER]: initCustom, - [EModelEndpoint.openAI]: initOpenAI, - [EModelEndpoint.google]: initGoogle, - [EModelEndpoint.azureOpenAI]: initOpenAI, - [EModelEndpoint.anthropic]: initAnthropic, - [EModelEndpoint.bedrock]: getBedrockOptions, -}; - -/** - * Get the provider configuration and override endpoint based on the provider string - * @param {Object} params - * @param {string} params.provider - The provider string - * @param {AppConfig} params.appConfig - The application configuration - * @returns {{ - * getOptions: (typeof providerConfigMap)[keyof typeof providerConfigMap], - * overrideProvider: string, - * customEndpointConfig?: TEndpoint - * }} - */ -function getProviderConfig({ provider, appConfig }) { - let getOptions = providerConfigMap[provider]; - let overrideProvider = provider; - /** @type {TEndpoint | undefined} */ - let customEndpointConfig; - - if (!getOptions && providerConfigMap[provider.toLowerCase()] != null) { - overrideProvider = provider.toLowerCase(); - getOptions = providerConfigMap[overrideProvider]; - } else if (!getOptions) { - customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig }); - if (!customEndpointConfig) { - throw new Error(`Provider ${provider} not supported`); - } - getOptions = initCustom; - overrideProvider = Providers.OPENAI; - } - - if (isKnownCustomProvider(overrideProvider) && !customEndpointConfig) { - customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig }); - if (!customEndpointConfig) { - throw new Error(`Provider ${provider} not supported`); - } - } - - return { - getOptions, - overrideProvider, - customEndpointConfig, - }; -} - -module.exports = { - getProviderConfig, -}; diff --git a/client/src/a11y/LiveAnnouncer.tsx b/client/src/a11y/LiveAnnouncer.tsx index 0eac8089bc..ac83ff2962 100644 --- a/client/src/a11y/LiveAnnouncer.tsx +++ b/client/src/a11y/LiveAnnouncer.tsx @@ -21,6 +21,9 @@ const LiveAnnouncer: React.FC = ({ children }) => { start: localize('com_a11y_start'), end: localize('com_a11y_end'), composing: localize('com_a11y_ai_composing'), + summarize_started: localize('com_a11y_summarize_started'), + summarize_completed: localize('com_a11y_summarize_completed'), + summarize_failed: localize('com_a11y_summarize_failed'), }), [localize], ); diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index 7bce7ac11d..d0c7d2af37 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -8,7 +8,15 @@ import { } from 'librechat-data-provider'; import { memo } from 'react'; import type { TMessageContentParts, TAttachment } from 'librechat-data-provider'; -import { OpenAIImageGen, EmptyText, Reasoning, ExecuteCode, AgentUpdate, Text } from './Parts'; +import { + OpenAIImageGen, + ExecuteCode, + AgentUpdate, + EmptyText, + Reasoning, + Summary, + Text, +} from './Parts'; import { ErrorMessage } from './MessageContent'; import RetrievalCall from './RetrievalCall'; import { getCachedPreview } from '~/utils'; @@ -100,6 +108,16 @@ const Part = memo(function Part({ return null; } return ; + } else if (part.type === ContentTypes.SUMMARY) { + return ( + + ); } else if (part.type === ContentTypes.TOOL_CALL) { const toolCall = part[ContentTypes.TOOL_CALL]; diff --git a/client/src/components/Chat/Messages/Content/Parts/Summary.tsx b/client/src/components/Chat/Messages/Content/Parts/Summary.tsx new file mode 100644 index 0000000000..77973f0c06 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/Summary.tsx @@ -0,0 +1,327 @@ +import { memo, useMemo, useState, useCallback, useRef, useId, useEffect } from 'react'; +import { useAtomValue } from 'jotai'; +import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client'; +import { ScrollText, ChevronDown, ChevronUp } from 'lucide-react'; +import type { MouseEvent, FocusEvent } from 'react'; +import type { SummaryContentPart } from 'librechat-data-provider'; +import { fontSizeAtom } from '~/store/fontSize'; +import { useMessageContext } from '~/Providers'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; + +type SummaryProps = Pick< + SummaryContentPart, + 'content' | 'model' | 'provider' | 'tokenCount' | 'summarizing' +>; + +function useCopyToClipboard(content?: string) { + const [isCopied, setIsCopied] = useState(false); + const timerRef = useRef>(); + useEffect(() => () => clearTimeout(timerRef.current), []); + const handleCopy = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + if (content) { + navigator.clipboard.writeText(content).then( + () => { + clearTimeout(timerRef.current); + setIsCopied(true); + timerRef.current = setTimeout(() => setIsCopied(false), 2000); + }, + () => { + /* clipboard permission denied — leave icon unchanged */ + }, + ); + } + }, + [content], + ); + return { isCopied, handleCopy }; +} + +const SummaryContent = memo(({ children, meta }: { children: React.ReactNode; meta?: string }) => { + const fontSize = useAtomValue(fontSizeAtom); + + return ( +
+ {meta && {meta}} +

{children}

+
+ ); +}); + +const SummaryButton = memo( + ({ + isExpanded, + onClick, + label, + content, + contentId, + showCopyButton = true, + isCopied, + onCopy, + }: { + isExpanded: boolean; + onClick: (e: MouseEvent) => void; + label: string; + content?: string; + contentId: string; + showCopyButton?: boolean; + isCopied: boolean; + onCopy: (e: MouseEvent) => void; + }) => { + const localize = useLocalize(); + const fontSize = useAtomValue(fontSizeAtom); + + return ( +
+ + {content && showCopyButton && ( + + )} +
+ ); + }, +); + +const FloatingSummaryBar = memo( + ({ + isVisible, + onClick, + content, + contentId, + isCopied, + onCopy, + }: { + isVisible: boolean; + onClick: (e: MouseEvent) => void; + content?: string; + contentId: string; + isCopied: boolean; + onCopy: (e: MouseEvent) => void; + }) => { + const localize = useLocalize(); + + const collapseTooltip = localize('com_ui_collapse_summary'); + const copyTooltip = isCopied + ? localize('com_ui_copied_to_clipboard') + : localize('com_ui_copy_summary'); + + return ( +
+ +
+ ); + }, +); + +const Summary = memo(({ content, model, provider, tokenCount, summarizing }: SummaryProps) => { + const contentId = useId(); + const localize = useLocalize(); + const [isExpanded, setIsExpanded] = useState(false); + const [isBarVisible, setIsBarVisible] = useState(false); + const containerRef = useRef(null); + const { isSubmitting, isLatestMessage } = useMessageContext(); + + const text = useMemo( + () => + (content ?? []) + .map((block) => ('text' in block && typeof block.text === 'string' ? block.text : '')) + .join(''), + [content], + ); + const { isCopied, handleCopy } = useCopyToClipboard(text); + + const handleClick = useCallback((e: MouseEvent) => { + e.preventDefault(); + setIsExpanded((prev) => !prev); + }, []); + + const handleFocus = useCallback(() => setIsBarVisible(true), []); + const handleBlur = useCallback((e: FocusEvent) => { + if (!containerRef.current?.contains(e.relatedTarget as Node)) { + setIsBarVisible(false); + } + }, []); + const handleMouseEnter = useCallback(() => setIsBarVisible(true), []); + const handleMouseLeave = useCallback(() => { + if (!containerRef.current?.contains(document.activeElement)) { + setIsBarVisible(false); + } + }, []); + + const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; + const isActivelyStreaming = !!summarizing && !!effectiveIsSubmitting; + + const meta = useMemo(() => { + const parts: string[] = []; + if (provider || model) { + parts.push([provider, model].filter(Boolean).join('/')); + } + if (tokenCount != null && tokenCount > 0) { + parts.push(`${tokenCount} ${localize('com_ui_tokens')}`); + } + return parts.length > 0 ? parts.join(' \u00b7 ') : undefined; + }, [model, provider, tokenCount, localize]); + + const label = useMemo( + () => + isActivelyStreaming + ? localize('com_ui_summarizing') + : localize('com_ui_conversation_summarized'), + [isActivelyStreaming, localize], + ); + + if (!summarizing && !text) { + return null; + } + + return ( +
+
+
+ +
+
+
+ {text} + +
+
+
+
+ ); +}); + +SummaryContent.displayName = 'SummaryContent'; +SummaryButton.displayName = 'SummaryButton'; +FloatingSummaryBar.displayName = 'FloatingSummaryBar'; +Summary.displayName = 'Summary'; + +export default Summary; diff --git a/client/src/components/Chat/Messages/Content/Parts/index.ts b/client/src/components/Chat/Messages/Content/Parts/index.ts index 8788201e65..b0a418c819 100644 --- a/client/src/components/Chat/Messages/Content/Parts/index.ts +++ b/client/src/components/Chat/Messages/Content/Parts/index.ts @@ -6,5 +6,6 @@ export { default as Reasoning } from './Reasoning'; export { default as EmptyText } from './EmptyText'; export { default as LogContent } from './LogContent'; export { default as ExecuteCode } from './ExecuteCode'; +export { default as Summary } from './Summary'; export { default as AgentUpdate } from './AgentUpdate'; export { default as EditTextPart } from './EditTextPart'; diff --git a/client/src/hooks/SSE/__tests__/useStepHandler.spec.ts b/client/src/hooks/SSE/__tests__/useStepHandler.spec.ts index cbe13f3910..220d55704d 100644 --- a/client/src/hooks/SSE/__tests__/useStepHandler.spec.ts +++ b/client/src/hooks/SSE/__tests__/useStepHandler.spec.ts @@ -1,7 +1,8 @@ import { renderHook, act } from '@testing-library/react'; -import { StepTypes, ContentTypes, ToolCallTypes } from 'librechat-data-provider'; +import { StepTypes, StepEvents, ContentTypes, ToolCallTypes } from 'librechat-data-provider'; import type { TMessageContentParts, + SummaryContentPart, EventSubmission, TEndpointOption, TConversation, @@ -155,7 +156,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -174,7 +175,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(consoleSpy).toHaveBeenCalledWith('No message id found in run step event'); @@ -194,7 +195,7 @@ describe('useStepHandler', () => { }); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -210,7 +211,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -235,7 +236,7 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta(stepId, 'Hello') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta(stepId, 'Hello') }, submission, ); }); @@ -245,7 +246,7 @@ describe('useStepHandler', () => { const runStep = createRunStep({ id: stepId }); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -266,7 +267,7 @@ describe('useStepHandler', () => { const submission = createSubmission({ userMessage: userMsg }); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -289,7 +290,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; @@ -315,7 +316,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -330,7 +331,10 @@ describe('useStepHandler', () => { }; act(() => { - result.current.stepHandler({ event: 'on_agent_update', data: agentUpdate }, submission); + result.current.stepHandler( + { event: StepEvents.ON_AGENT_UPDATE, data: agentUpdate }, + submission, + ); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -352,7 +356,10 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_agent_update', data: agentUpdate }, submission); + result.current.stepHandler( + { event: StepEvents.ON_AGENT_UPDATE, data: agentUpdate }, + submission, + ); }); expect(consoleSpy).toHaveBeenCalledWith('No message id found in agent update event'); @@ -371,7 +378,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -379,7 +386,10 @@ describe('useStepHandler', () => { const messageDelta = createMessageDelta('step-1', 'Hello'); act(() => { - result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_MESSAGE_DELTA, data: messageDelta }, + submission, + ); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -397,7 +407,10 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_MESSAGE_DELTA, data: messageDelta }, + submission, + ); }); expect(mockSetMessages).not.toHaveBeenCalled(); @@ -413,19 +426,19 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); act(() => { result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta('step-1', 'Hello ') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta('step-1', 'Hello ') }, submission, ); }); act(() => { result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta('step-1', 'World') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta('step-1', 'World') }, submission, ); }); @@ -447,7 +460,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -458,7 +471,10 @@ describe('useStepHandler', () => { }; act(() => { - result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_MESSAGE_DELTA, data: messageDelta }, + submission, + ); }); expect(mockSetMessages).not.toHaveBeenCalled(); @@ -476,7 +492,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -485,7 +501,7 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( - { event: 'on_reasoning_delta', data: reasoningDelta }, + { event: StepEvents.ON_REASONING_DELTA, data: reasoningDelta }, submission, ); }); @@ -506,7 +522,7 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( - { event: 'on_reasoning_delta', data: reasoningDelta }, + { event: StepEvents.ON_REASONING_DELTA, data: reasoningDelta }, submission, ); }); @@ -524,19 +540,19 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); act(() => { result.current.stepHandler( - { event: 'on_reasoning_delta', data: createReasoningDelta('step-1', 'First ') }, + { event: StepEvents.ON_REASONING_DELTA, data: createReasoningDelta('step-1', 'First ') }, submission, ); }); act(() => { result.current.stepHandler( - { event: 'on_reasoning_delta', data: createReasoningDelta('step-1', 'thought') }, + { event: StepEvents.ON_REASONING_DELTA, data: createReasoningDelta('step-1', 'thought') }, submission, ); }); @@ -560,7 +576,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -574,7 +590,10 @@ describe('useStepHandler', () => { }; act(() => { - result.current.stepHandler({ event: 'on_run_step_delta', data: runStepDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_RUN_STEP_DELTA, data: runStepDelta }, + submission, + ); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -593,7 +612,10 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step_delta', data: runStepDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_RUN_STEP_DELTA, data: runStepDelta }, + submission, + ); }); expect(mockSetMessages).not.toHaveBeenCalled(); @@ -609,7 +631,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -625,7 +647,10 @@ describe('useStepHandler', () => { }; act(() => { - result.current.stepHandler({ event: 'on_run_step_delta', data: runStepDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_RUN_STEP_DELTA, data: runStepDelta }, + submission, + ); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -649,7 +674,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -671,8 +696,8 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( { - event: 'on_run_step_completed', - data: completedEvent as unknown as Agents.ToolEndEvent, + event: StepEvents.ON_RUN_STEP_COMPLETED, + data: completedEvent as { result: Agents.ToolEndEvent }, }, submission, ); @@ -710,8 +735,8 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( { - event: 'on_run_step_completed', - data: completedEvent as unknown as Agents.ToolEndEvent, + event: StepEvents.ON_RUN_STEP_COMPLETED, + data: completedEvent as { result: Agents.ToolEndEvent }, }, submission, ); @@ -735,7 +760,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); act(() => { @@ -746,7 +771,7 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta('step-1', 'Test') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta('step-1', 'Test') }, submission, ); }); @@ -772,12 +797,12 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); act(() => { result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta('step-1', ' more') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta('step-1', ' more') }, submission, ); }); @@ -824,7 +849,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockAnnouncePolite).toHaveBeenCalledWith({ message: 'composing', isStatus: true }); @@ -842,7 +867,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockAnnouncePolite).not.toHaveBeenCalled(); @@ -872,7 +897,7 @@ describe('useStepHandler', () => { }); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -891,15 +916,15 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta(stepId, 'First ') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta(stepId, 'First ') }, submission, ); result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta(stepId, 'Second ') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta(stepId, 'Second ') }, submission, ); result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta(stepId, 'Third') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta(stepId, 'Third') }, submission, ); }); @@ -909,7 +934,7 @@ describe('useStepHandler', () => { const runStep = createRunStep({ id: stepId }); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -931,11 +956,14 @@ describe('useStepHandler', () => { act(() => { result.current.stepHandler( - { event: 'on_reasoning_delta', data: createReasoningDelta(stepId, 'Thinking...') }, + { + event: StepEvents.ON_REASONING_DELTA, + data: createReasoningDelta(stepId, 'Thinking...'), + }, submission, ); result.current.stepHandler( - { event: 'on_message_delta', data: createMessageDelta(stepId, 'Response') }, + { event: StepEvents.ON_MESSAGE_DELTA, data: createMessageDelta(stepId, 'Response') }, submission, ); }); @@ -945,7 +973,7 @@ describe('useStepHandler', () => { const runStep = createRunStep({ id: stepId }); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -971,7 +999,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); const textDelta: Agents.MessageDeltaEvent = { @@ -980,7 +1008,10 @@ describe('useStepHandler', () => { }; act(() => { - result.current.stepHandler({ event: 'on_message_delta', data: textDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_MESSAGE_DELTA, data: textDelta }, + submission, + ); }); expect(consoleSpy).toHaveBeenCalledWith( @@ -994,6 +1025,395 @@ describe('useStepHandler', () => { }); }); + describe('summarization events', () => { + it('ON_SUMMARIZE_START calls announcePolite', () => { + mockLastAnnouncementTimeRef.current = Date.now(); + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_START, + data: { + agentId: 'agent-1', + provider: 'test-provider', + model: 'test-model', + messagesToRefineCount: 5, + summaryVersion: 1, + }, + }, + submission, + ); + }); + + expect(mockAnnouncePolite).toHaveBeenCalledWith({ + message: 'summarize_started', + isStatus: true, + }); + }); + + it('ON_SUMMARIZE_DELTA accumulates content on known run step', async () => { + mockLastAnnouncementTimeRef.current = Date.now(); + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + const submission = createSubmission(); + + const runStep = createRunStep({ + summary: { + type: ContentTypes.SUMMARY, + model: 'test-model', + provider: 'test-provider', + } as TMessageContentParts & { type: typeof ContentTypes.SUMMARY }, + }); + + act(() => { + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_DELTA, + data: { + id: 'step-1', + delta: { + summary: { + type: ContentTypes.SUMMARY, + content: [{ type: ContentTypes.TEXT, text: 'chunk1' }], + provider: 'test-provider', + model: 'test-model', + summarizing: true, + }, + }, + }, + }, + submission, + ); + }); + + await act(async () => { + await new Promise((r) => requestAnimationFrame(r)); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall[lastCall.length - 1]; + const summaryPart = responseMsg.content?.find( + (c: TMessageContentParts) => c.type === ContentTypes.SUMMARY, + ); + expect(summaryPart).toBeDefined(); + expect(summaryPart.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.TEXT, text: 'chunk1' }), + ); + }); + + it('ON_SUMMARIZE_DELTA buffers when run step is not yet known', () => { + mockLastAnnouncementTimeRef.current = Date.now(); + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_DELTA, + data: { + id: 'step-1', + delta: { + summary: { + type: ContentTypes.SUMMARY, + content: [{ type: ContentTypes.TEXT, text: 'buffered chunk' }], + provider: 'test-provider', + model: 'test-model', + summarizing: true, + }, + }, + }, + }, + submission, + ); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + }); + + it('ON_SUMMARIZE_COMPLETE success replaces summarizing part with finalized summary', () => { + mockLastAnnouncementTimeRef.current = Date.now(); + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + const submission = createSubmission(); + + const runStep = createRunStep({ + summary: { + type: ContentTypes.SUMMARY, + model: 'test-model', + provider: 'test-provider', + } as TMessageContentParts & { type: typeof ContentTypes.SUMMARY }, + }); + + act(() => { + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); + }); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_DELTA, + data: { + id: 'step-1', + delta: { + summary: { + type: ContentTypes.SUMMARY, + content: [{ type: ContentTypes.TEXT, text: 'partial' }], + provider: 'test-provider', + model: 'test-model', + summarizing: true, + }, + }, + }, + }, + submission, + ); + }); + + mockSetMessages.mockClear(); + mockAnnouncePolite.mockClear(); + + const lastSetCall = mockGetMessages.mock.results[mockGetMessages.mock.results.length - 1]; + const latestMessages = lastSetCall?.value ?? []; + mockGetMessages.mockReturnValue( + latestMessages.length > 0 ? latestMessages : [responseMessage], + ); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_COMPLETE, + data: { + id: 'step-1', + agentId: 'agent-1', + summary: { + type: ContentTypes.SUMMARY, + content: [{ type: ContentTypes.TEXT, text: 'Final summary' }], + tokenCount: 100, + summarizing: false, + }, + }, + }, + submission, + ); + }); + + expect(mockAnnouncePolite).toHaveBeenCalledWith({ + message: 'summarize_completed', + isStatus: true, + }); + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => m.messageId === 'response-msg-1'); + const summaryPart = responseMsg?.content?.find( + (c: TMessageContentParts) => c.type === ContentTypes.SUMMARY, + ); + expect(summaryPart).toMatchObject({ summarizing: false }); + }); + + it('ON_SUMMARIZE_COMPLETE error removes summarizing parts', () => { + mockLastAnnouncementTimeRef.current = Date.now(); + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + const submission = createSubmission(); + + const runStep = createRunStep({ + summary: { + type: ContentTypes.SUMMARY, + model: 'test-model', + provider: 'test-provider', + } as TMessageContentParts & { type: typeof ContentTypes.SUMMARY }, + }); + + act(() => { + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); + }); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_DELTA, + data: { + id: 'step-1', + delta: { + summary: { + type: ContentTypes.SUMMARY, + content: [{ type: ContentTypes.TEXT, text: 'partial' }], + provider: 'test-provider', + model: 'test-model', + summarizing: true, + }, + }, + }, + }, + submission, + ); + }); + + mockSetMessages.mockClear(); + mockAnnouncePolite.mockClear(); + + const lastSetCall = mockGetMessages.mock.results[mockGetMessages.mock.results.length - 1]; + const latestMessages = lastSetCall?.value ?? []; + mockGetMessages.mockReturnValue( + latestMessages.length > 0 ? latestMessages : [responseMessage], + ); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_COMPLETE, + data: { + id: 'step-1', + agentId: 'agent-1', + error: 'LLM failed', + }, + }, + submission, + ); + }); + + expect(mockAnnouncePolite).toHaveBeenCalledWith({ + message: 'summarize_failed', + isStatus: true, + }); + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => m.messageId === 'response-msg-1'); + const summaryParts = + responseMsg?.content?.filter( + (c: TMessageContentParts) => c.type === ContentTypes.SUMMARY, + ) ?? []; + expect(summaryParts).toHaveLength(0); + }); + + it('ON_SUMMARIZE_COMPLETE returns early when target message not in messageMap', () => { + mockLastAnnouncementTimeRef.current = Date.now(); + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_COMPLETE, + data: { + id: 'step-1', + agentId: 'agent-1', + summary: { + type: ContentTypes.SUMMARY, + content: [{ type: ContentTypes.TEXT, text: 'Final summary' }], + tokenCount: 100, + summarizing: false, + }, + }, + }, + submission, + ); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + expect(mockAnnouncePolite).not.toHaveBeenCalled(); + }); + + it('ON_SUMMARIZE_COMPLETE with undefined summary finalizes existing part with summarizing=false', () => { + mockLastAnnouncementTimeRef.current = Date.now(); + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + const submission = createSubmission(); + + const runStep = createRunStep({ + summary: { + type: ContentTypes.SUMMARY, + model: 'test-model', + provider: 'test-provider', + } as TMessageContentParts & { type: typeof ContentTypes.SUMMARY }, + }); + + act(() => { + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); + }); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_DELTA, + data: { + id: 'step-1', + delta: { + summary: { + type: ContentTypes.SUMMARY, + content: [{ type: ContentTypes.TEXT, text: 'partial' }], + provider: 'test-provider', + model: 'test-model', + summarizing: true, + }, + }, + }, + }, + submission, + ); + }); + + mockSetMessages.mockClear(); + mockAnnouncePolite.mockClear(); + + const lastSetCall = mockGetMessages.mock.results[mockGetMessages.mock.results.length - 1]; + const latestMessages = lastSetCall?.value ?? []; + mockGetMessages.mockReturnValue( + latestMessages.length > 0 ? latestMessages : [responseMessage], + ); + + act(() => { + result.current.stepHandler( + { + event: StepEvents.ON_SUMMARIZE_COMPLETE, + data: { + id: 'step-1', + agentId: 'agent-1', + }, + }, + submission, + ); + }); + + expect(mockAnnouncePolite).toHaveBeenCalledWith({ + message: 'summarize_completed', + isStatus: true, + }); + expect(mockSetMessages).toHaveBeenCalledTimes(1); + const updatedMessages = mockSetMessages.mock.calls[0][0] as TMessage[]; + const summaryPart = updatedMessages[0]?.content?.find( + (p: TMessageContentParts) => p?.type === ContentTypes.SUMMARY, + ) as SummaryContentPart | undefined; + expect(summaryPart?.summarizing).toBe(false); + }); + }); + describe('edge cases', () => { it('should handle empty messages array', () => { mockGetMessages.mockReturnValue([]); @@ -1004,7 +1424,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -1019,7 +1439,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -1035,7 +1455,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); const messageDelta: Agents.MessageDeltaEvent = { @@ -1049,7 +1469,10 @@ describe('useStepHandler', () => { }; act(() => { - result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_MESSAGE_DELTA, data: messageDelta }, + submission, + ); }); expect(mockSetMessages).toHaveBeenCalled(); @@ -1065,7 +1488,7 @@ describe('useStepHandler', () => { const submission = createSubmission(); act(() => { - result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + result.current.stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, submission); }); mockSetMessages.mockClear(); @@ -1076,7 +1499,10 @@ describe('useStepHandler', () => { }; act(() => { - result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + result.current.stepHandler( + { event: StepEvents.ON_MESSAGE_DELTA, data: messageDelta }, + submission, + ); }); expect(mockSetMessages).not.toHaveBeenCalled(); diff --git a/client/src/hooks/SSE/useResumableSSE.ts b/client/src/hooks/SSE/useResumableSSE.ts index ddfee30120..32820f8392 100644 --- a/client/src/hooks/SSE/useResumableSSE.ts +++ b/client/src/hooks/SSE/useResumableSSE.ts @@ -8,6 +8,7 @@ import { Constants, QueryKeys, ErrorTypes, + StepEvents, apiBaseUrl, createPayload, ViolationTypes, @@ -224,7 +225,7 @@ export default function useResumableSSE( if (data.resumeState?.runSteps) { for (const runStep of data.resumeState.runSteps) { - stepHandler({ event: 'on_run_step', data: runStep }, { + stepHandler({ event: StepEvents.ON_RUN_STEP, data: runStep }, { ...currentSubmission, userMessage, } as EventSubmission); diff --git a/client/src/hooks/SSE/useStepHandler.ts b/client/src/hooks/SSE/useStepHandler.ts index c3b48cb107..1f28d97433 100644 --- a/client/src/hooks/SSE/useStepHandler.ts +++ b/client/src/hooks/SSE/useStepHandler.ts @@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react'; import { Constants, StepTypes, + StepEvents, ContentTypes, ToolCallTypes, getNonEmptyValue, @@ -12,6 +13,7 @@ import type { PartMetadata, ContentMetadata, EventSubmission, + SummaryContentPart, TMessageContentParts, } from 'librechat-data-provider'; import type { SetterOrUpdater } from 'recoil'; @@ -27,20 +29,16 @@ type TUseStepHandler = { lastAnnouncementTimeRef: React.MutableRefObject; }; -type TStepEvent = { - event: string; - data: - | Agents.MessageDeltaEvent - | Agents.ReasoningDeltaEvent - | Agents.RunStepDeltaEvent - | Agents.AgentUpdate - | Agents.RunStep - | Agents.ToolEndEvent - | { - runId?: string; - message: string; - }; -}; +type TStepEvent = + | { event: StepEvents.ON_RUN_STEP; data: Agents.RunStep } + | { event: StepEvents.ON_AGENT_UPDATE; data: Agents.AgentUpdate } + | { event: StepEvents.ON_MESSAGE_DELTA; data: Agents.MessageDeltaEvent } + | { event: StepEvents.ON_REASONING_DELTA; data: Agents.ReasoningDeltaEvent } + | { event: StepEvents.ON_RUN_STEP_DELTA; data: Agents.RunStepDeltaEvent } + | { event: StepEvents.ON_RUN_STEP_COMPLETED; data: { result: Agents.ToolEndEvent } } + | { event: StepEvents.ON_SUMMARIZE_START; data: Agents.SummarizeStartEvent } + | { event: StepEvents.ON_SUMMARIZE_DELTA; data: Agents.SummarizeDeltaEvent } + | { event: StepEvents.ON_SUMMARIZE_COMPLETE; data: Agents.SummarizeCompleteEvent }; type MessageDeltaUpdate = { type: ContentTypes.TEXT; text: string; tool_call_ids?: string[] }; @@ -52,6 +50,7 @@ type AllContentTypes = | ContentTypes.TOOL_CALL | ContentTypes.IMAGE_FILE | ContentTypes.IMAGE_URL + | ContentTypes.SUMMARY | ContentTypes.ERROR; export default function useStepHandler({ @@ -65,6 +64,8 @@ export default function useStepHandler({ const stepMap = useRef(new Map()); /** Buffer for deltas that arrive before their corresponding run step */ const pendingDeltaBuffer = useRef(new Map()); + /** Coalesces rapid-fire summarize delta renders into a single rAF frame */ + const summarizeDeltaRaf = useRef(null); /** * Calculate content index for a run step. @@ -138,7 +139,7 @@ export default function useStepHandler({ text: (currentContent.text || '') + contentPart.text, }; - if (contentPart.tool_call_ids != null) { + if ('tool_call_ids' in contentPart && contentPart.tool_call_ids != null) { update.tool_call_ids = contentPart.tool_call_ids; } updatedContent[index] = update; @@ -173,6 +174,13 @@ export default function useStepHandler({ updatedContent[index] = { ...currentContent, }; + } else if (contentType === ContentTypes.SUMMARY) { + const currentSummary = updatedContent[index] as SummaryContentPart | undefined; + const incoming = contentPart as SummaryContentPart; + updatedContent[index] = { + ...incoming, + content: [...(currentSummary?.content ?? []), ...(incoming.content ?? [])], + }; } else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) { const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined; const existingToolCall = existingContent?.tool_call; @@ -243,7 +251,7 @@ export default function useStepHandler({ }; const stepHandler = useCallback( - ({ event, data }: TStepEvent, submission: EventSubmission) => { + (stepEvent: TStepEvent, submission: EventSubmission) => { const messages = getMessages() || []; const { userMessage } = submission; let parentMessageId = userMessage.messageId; @@ -260,8 +268,8 @@ export default function useStepHandler({ initialContent = submission?.initialResponse?.content ?? initialContent; } - if (event === 'on_run_step') { - const runStep = data as Agents.RunStep; + if (stepEvent.event === StepEvents.ON_RUN_STEP) { + const runStep = stepEvent.data; let responseMessageId = runStep.runId ?? ''; if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) { responseMessageId = submission?.initialResponse?.messageId ?? ''; @@ -355,15 +363,38 @@ export default function useStepHandler({ setMessages(updatedMessages); } + if (runStep.summary != null) { + const summaryPart: SummaryContentPart = { + type: ContentTypes.SUMMARY, + content: [], + summarizing: true, + model: runStep.summary.model, + provider: runStep.summary.provider, + }; + + let updatedResponse = { ...(messageMap.current.get(responseMessageId) ?? response) }; + updatedResponse = updateContent( + updatedResponse, + contentIndex, + summaryPart, + false, + getStepMetadata(runStep), + ); + + messageMap.current.set(responseMessageId, updatedResponse); + const currentMessages = getMessages() || []; + setMessages([...currentMessages.slice(0, -1), updatedResponse]); + } + const bufferedDeltas = pendingDeltaBuffer.current.get(runStep.id); if (bufferedDeltas && bufferedDeltas.length > 0) { pendingDeltaBuffer.current.delete(runStep.id); for (const bufferedDelta of bufferedDeltas) { - stepHandler({ event: bufferedDelta.event, data: bufferedDelta.data }, submission); + stepHandler(bufferedDelta, submission); } } - } else if (event === 'on_agent_update') { - const { agent_update } = data as Agents.AgentUpdate; + } else if (stepEvent.event === StepEvents.ON_AGENT_UPDATE) { + const { agent_update } = stepEvent.data; let responseMessageId = agent_update.runId || ''; if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) { responseMessageId = submission?.initialResponse?.messageId ?? ''; @@ -385,7 +416,7 @@ export default function useStepHandler({ const updatedResponse = updateContent( response, currentIndex, - data, + stepEvent.data, false, agentUpdateMeta, ); @@ -393,8 +424,8 @@ export default function useStepHandler({ const currentMessages = getMessages() || []; setMessages([...currentMessages.slice(0, -1), updatedResponse]); } - } else if (event === 'on_message_delta') { - const messageDelta = data as Agents.MessageDeltaEvent; + } else if (stepEvent.event === StepEvents.ON_MESSAGE_DELTA) { + const messageDelta = stepEvent.data; const runStep = stepMap.current.get(messageDelta.id); let responseMessageId = runStep?.runId ?? ''; if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) { @@ -404,7 +435,7 @@ export default function useStepHandler({ if (!runStep || !responseMessageId) { const buffer = pendingDeltaBuffer.current.get(messageDelta.id) ?? []; - buffer.push({ event: 'on_message_delta', data: messageDelta }); + buffer.push({ event: StepEvents.ON_MESSAGE_DELTA, data: messageDelta }); pendingDeltaBuffer.current.set(messageDelta.id, buffer); return; } @@ -436,8 +467,8 @@ export default function useStepHandler({ const currentMessages = getMessages() || []; setMessages([...currentMessages.slice(0, -1), updatedResponse]); } - } else if (event === 'on_reasoning_delta') { - const reasoningDelta = data as Agents.ReasoningDeltaEvent; + } else if (stepEvent.event === StepEvents.ON_REASONING_DELTA) { + const reasoningDelta = stepEvent.data; const runStep = stepMap.current.get(reasoningDelta.id); let responseMessageId = runStep?.runId ?? ''; if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) { @@ -447,7 +478,7 @@ export default function useStepHandler({ if (!runStep || !responseMessageId) { const buffer = pendingDeltaBuffer.current.get(reasoningDelta.id) ?? []; - buffer.push({ event: 'on_reasoning_delta', data: reasoningDelta }); + buffer.push({ event: StepEvents.ON_REASONING_DELTA, data: reasoningDelta }); pendingDeltaBuffer.current.set(reasoningDelta.id, buffer); return; } @@ -479,8 +510,8 @@ export default function useStepHandler({ const currentMessages = getMessages() || []; setMessages([...currentMessages.slice(0, -1), updatedResponse]); } - } else if (event === 'on_run_step_delta') { - const runStepDelta = data as Agents.RunStepDeltaEvent; + } else if (stepEvent.event === StepEvents.ON_RUN_STEP_DELTA) { + const runStepDelta = stepEvent.data; const runStep = stepMap.current.get(runStepDelta.id); let responseMessageId = runStep?.runId ?? ''; if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) { @@ -490,7 +521,7 @@ export default function useStepHandler({ if (!runStep || !responseMessageId) { const buffer = pendingDeltaBuffer.current.get(runStepDelta.id) ?? []; - buffer.push({ event: 'on_run_step_delta', data: runStepDelta }); + buffer.push({ event: StepEvents.ON_RUN_STEP_DELTA, data: runStepDelta }); pendingDeltaBuffer.current.set(runStepDelta.id, buffer); return; } @@ -538,8 +569,8 @@ export default function useStepHandler({ setMessages(updatedMessages); } - } else if (event === 'on_run_step_completed') { - const { result } = data as unknown as { result: Agents.ToolEndEvent }; + } else if (stepEvent.event === StepEvents.ON_RUN_STEP_COMPLETED) { + const { result } = stepEvent.data; const { id: stepId } = result; @@ -581,18 +612,116 @@ export default function useStepHandler({ setMessages(updatedMessages); } - } + } else if (stepEvent.event === StepEvents.ON_SUMMARIZE_START) { + announcePolite({ message: 'summarize_started', isStatus: true }); + } else if (stepEvent.event === StepEvents.ON_SUMMARIZE_DELTA) { + const deltaData = stepEvent.data; + const runStep = stepMap.current.get(deltaData.id); + let responseMessageId = runStep?.runId ?? ''; + if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) { + responseMessageId = submission?.initialResponse?.messageId ?? ''; + parentMessageId = submission?.initialResponse?.parentMessageId ?? ''; + } - return () => { - toolCallIdMap.current.clear(); - messageMap.current.clear(); - stepMap.current.clear(); - }; + if (!runStep || !responseMessageId) { + const buffer = pendingDeltaBuffer.current.get(deltaData.id) ?? []; + buffer.push({ event: StepEvents.ON_SUMMARIZE_DELTA, data: deltaData }); + pendingDeltaBuffer.current.set(deltaData.id, buffer); + return; + } + + const response = messageMap.current.get(responseMessageId); + if (response) { + const contentPart: SummaryContentPart = { + ...deltaData.delta.summary, + summarizing: true, + }; + + const contentIndex = runStep.index + initialContent.length; + const updatedResponse = updateContent( + response, + contentIndex, + contentPart, + false, + getStepMetadata(runStep), + ); + messageMap.current.set(responseMessageId, updatedResponse); + if (summarizeDeltaRaf.current == null) { + summarizeDeltaRaf.current = requestAnimationFrame(() => { + summarizeDeltaRaf.current = null; + const latest = messageMap.current.get(responseMessageId); + if (latest) { + const msgs = getMessages() || []; + setMessages([...msgs.slice(0, -1), latest]); + } + }); + } + } + } else if (stepEvent.event === StepEvents.ON_SUMMARIZE_COMPLETE) { + const completeData = stepEvent.data; + const completeRunStep = stepMap.current.get(completeData.id); + let completeMessageId = completeRunStep?.runId ?? ''; + if (completeMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) { + completeMessageId = submission?.initialResponse?.messageId ?? ''; + } + + const targetMessage = messageMap.current.get(completeMessageId); + if (!targetMessage || !Array.isArray(targetMessage.content)) { + return; + } + + const currentMessages = getMessages() || []; + const targetIndex = currentMessages.findIndex((m) => m.messageId === completeMessageId); + + if (completeData.error) { + const filtered = targetMessage.content.filter( + (part) => + part?.type !== ContentTypes.SUMMARY || !(part as SummaryContentPart).summarizing, + ); + if (filtered.length !== targetMessage.content.length) { + announcePolite({ message: 'summarize_failed', isStatus: true }); + const cleaned = { ...targetMessage, content: filtered }; + messageMap.current.set(completeMessageId, cleaned); + if (targetIndex >= 0) { + const updated = [...currentMessages]; + updated[targetIndex] = cleaned; + setMessages(updated); + } + } + } else { + let didFinalize = false; + const updatedContent = targetMessage.content.map((part) => { + if (part?.type === ContentTypes.SUMMARY && (part as SummaryContentPart).summarizing) { + didFinalize = true; + if (!completeData.summary) { + return { ...part, summarizing: false } as SummaryContentPart; + } + return { ...completeData.summary, summarizing: false } as SummaryContentPart; + } + return part; + }); + if (didFinalize && targetIndex >= 0) { + announcePolite({ message: 'summarize_completed', isStatus: true }); + const finalized = { ...targetMessage, content: updatedContent }; + messageMap.current.set(completeMessageId, finalized); + const updated = [...currentMessages]; + updated[targetIndex] = finalized; + setMessages(updated); + } + } + } else { + const _exhaustive: never = stepEvent; + console.warn('Unhandled step event', (_exhaustive as TStepEvent).event); + } }, [getMessages, lastAnnouncementTimeRef, announcePolite, setMessages, calculateContentIndex], ); const clearStepMaps = useCallback(() => { + if (summarizeDeltaRaf.current != null) { + cancelAnimationFrame(summarizeDeltaRaf.current); + summarizeDeltaRaf.current = null; + } toolCallIdMap.current.clear(); messageMap.current.clear(); stepMap.current.clear(); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 67111586ff..987ac314a9 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -5,6 +5,9 @@ "com_a11y_chats_date_section": "Chats from {{date}}", "com_a11y_end": "The AI has finished their reply.", "com_a11y_selected": "selected", + "com_a11y_summarize_completed": "Context summarized.", + "com_a11y_summarize_failed": "Summarization failed, continuing with available context.", + "com_a11y_summarize_started": "Summarizing context.", "com_a11y_start": "The AI has started their reply.", "com_agents_agent_card_label": "{{name}} agent. {{description}}", "com_agents_all": "All Agents", @@ -830,6 +833,7 @@ "com_ui_code": "Code", "com_ui_collapse": "Collapse", "com_ui_collapse_chat": "Collapse Chat", + "com_ui_collapse_summary": "Collapse Summary", "com_ui_collapse_thoughts": "Collapse Thoughts", "com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used", "com_ui_command_usage_placeholder": "Select a Prompt by command or name", @@ -852,6 +856,7 @@ "com_ui_conversation": "conversation", "com_ui_conversation_label": "{{title}} conversation", "com_ui_conversation_not_found": "Conversation not found", + "com_ui_conversation_summarized": "Conversation summarized", "com_ui_conversations": "conversations", "com_ui_convo_archived": "Conversation archived", "com_ui_convo_delete_error": "Failed to delete conversation", @@ -862,6 +867,7 @@ "com_ui_copy_code": "Copy code", "com_ui_copy_link": "Copy link", "com_ui_copy_stack_trace": "Copy stack trace", + "com_ui_copy_summary": "Copy summary to clipboard", "com_ui_copy_thoughts_to_clipboard": "Copy thoughts to clipboard", "com_ui_copy_to_clipboard": "Copy to clipboard", "com_ui_copy_url_to_clipboard": "Copy URL to clipboard", @@ -1414,6 +1420,7 @@ "com_ui_storage": "Storage", "com_ui_storage_filter_sort": "Filter and Sort by Storage", "com_ui_submit": "Submit", + "com_ui_summarizing": "Summarizing...", "com_ui_support_contact": "Support Contact", "com_ui_support_contact_email": "Email", "com_ui_support_contact_email_invalid": "Please enter a valid email address", diff --git a/package-lock.json b/package-lock.json index aad4e24fda..b7e09a628e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.57", + "@librechat/agents": "^3.1.62", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -3162,30 +3162,30 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1011.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1011.0.tgz", - "integrity": "sha512-yn5oRLLP1TsGLZqlnyqBjAVmiexYR8/rPG8D+rI5f5+UIvb3zHOmHLXA1m41H/sKXI4embmXfUjvArmjTmfsIw==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1014.0.tgz", + "integrity": "sha512-K0TmX1D6dIh4J2QtqUuEXxbyMmtHD+kwHvUg1JwDXaLXC7zJJlR0p1692YBh/eze9tHbuKqP/VWzUy6XX9IPGw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/eventstream-handler-node": "^3.972.11", "@aws-sdk/middleware-eventstream": "^3.972.8", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/middleware-websocket": "^3.972.13", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/token-providers": "3.1011.0", + "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/token-providers": "3.1014.0", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", @@ -3193,25 +3193,25 @@ "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -3536,19 +3536,19 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", - "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "version": "3.973.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.23.tgz", + "integrity": "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/xml-builder": "^3.972.15", + "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", @@ -3611,12 +3611,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", - "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.21.tgz", + "integrity": "sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", @@ -3627,20 +3627,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", - "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.23.tgz", + "integrity": "sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -3648,19 +3648,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", - "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.23.tgz", + "integrity": "sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-login": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-env": "^3.972.21", + "@aws-sdk/credential-provider-http": "^3.972.23", + "@aws-sdk/credential-provider-login": "^3.972.23", + "@aws-sdk/credential-provider-process": "^3.972.21", + "@aws-sdk/credential-provider-sso": "^3.972.23", + "@aws-sdk/credential-provider-web-identity": "^3.972.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -3673,13 +3673,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", - "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.23.tgz", + "integrity": "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", @@ -3692,17 +3692,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", - "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.24.tgz", + "integrity": "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-ini": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/credential-provider-env": "^3.972.21", + "@aws-sdk/credential-provider-http": "^3.972.23", + "@aws-sdk/credential-provider-ini": "^3.972.23", + "@aws-sdk/credential-provider-process": "^3.972.21", + "@aws-sdk/credential-provider-sso": "^3.972.23", + "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -3715,12 +3715,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", - "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.21.tgz", + "integrity": "sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -3732,32 +3732,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", - "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.23.tgz", + "integrity": "sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/token-providers": "3.1009.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1009.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", - "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", + "@aws-sdk/token-providers": "3.1014.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -3769,13 +3751,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", - "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.23.tgz", + "integrity": "sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -4096,15 +4078,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", - "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.24.tgz", + "integrity": "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.11", + "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", @@ -4207,44 +4189,44 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", - "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "version": "3.996.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.13.tgz", + "integrity": "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -4310,13 +4292,13 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", - "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.9.tgz", + "integrity": "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", + "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" @@ -4388,13 +4370,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1011.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1011.0.tgz", - "integrity": "sha512-WSfBVDQ9uyh1GCR+DxxgHEvAKv+beMIlSeJ2pMAG1HTci340+xbtz1VFwnTJ5qCxrMi+E4dyDMiSAhDvHnq73A==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1014.0.tgz", + "integrity": "sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -4498,12 +4480,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", - "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.10.tgz", + "integrity": "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", @@ -4523,19 +4505,39 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", - "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", + "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.4.1", + "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", @@ -11859,13 +11861,13 @@ } }, "node_modules/@librechat/agents": { - "version": "3.1.57", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.57.tgz", - "integrity": "sha512-fP/ZF7a7QL/MhXTfdzpG3cpOai9LSiKMiFX1X23o3t67Bqj9r5FuSVgu+UHDfO7o4Np82ZWw2nQJjcMJQbArLA==", + "version": "3.1.62", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.62.tgz", + "integrity": "sha512-QBZlJ4C89GmBg9w2qoWOWl1Y1xiRypUtIMBsL6eLPIsdbKHJ+GYO+076rfSD+tMqZB5ZbrxqPWOh+gxEXK1coQ==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", - "@aws-sdk/client-bedrock-runtime": "^3.980.0", + "@aws-sdk/client-bedrock-runtime": "^3.1013.0", "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.80", @@ -19185,9 +19187,9 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", - "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", + "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", @@ -19573,9 +19575,9 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.26", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", - "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "version": "4.4.27", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", + "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -19592,15 +19594,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.43", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", - "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "version": "4.4.44", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", + "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -19806,13 +19808,13 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.6", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", - "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", + "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", - "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", @@ -19950,13 +19952,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.42", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", - "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", + "version": "4.3.43", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", + "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -19965,16 +19967,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.45", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", - "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", + "version": "4.2.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", + "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.11", + "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -34347,16 +34349,6 @@ "path-to-regexp": "^8.1.0" } }, - "node_modules/nise/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -35340,9 +35332,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", "funding": [ { "type": "github", @@ -40706,9 +40698,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", + "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", "funding": [ { "type": "github", @@ -43985,7 +43977,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.57", + "@librechat/agents": "^3.1.62", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/package.json b/packages/api/package.json index 71bb27a3c4..a4e74a7a3c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -95,7 +95,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.57", + "@librechat/agents": "^3.1.62", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts b/packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts new file mode 100644 index 0000000000..370168ea5d --- /dev/null +++ b/packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts @@ -0,0 +1,282 @@ +import { estimateMediaTokensForMessage } from '../client'; + +jest.mock('@librechat/agents', () => ({ + ...jest.requireActual('@librechat/agents'), + extractImageDimensions: jest.fn((data: string) => { + if (data.includes('VALID_PNG')) { + return { width: 800, height: 600 }; + } + return null; + }), + estimateAnthropicImageTokens: jest.fn( + (w: number, h: number) => Math.ceil((w * h) / 750), + ), + estimateOpenAIImageTokens: jest.fn( + (w: number, h: number) => Math.ceil((w * h) / 512) + 85, + ), +})); + +const fakeTokenCount = (text: string) => Math.ceil(text.length / 4); + +describe('estimateMediaTokensForMessage', () => { + describe('non-array content', () => { + it('returns 0 for string content', () => { + expect(estimateMediaTokensForMessage('hello', false)).toBe(0); + }); + + it('returns 0 for null', () => { + expect(estimateMediaTokensForMessage(null, false)).toBe(0); + }); + + it('returns 0 for undefined', () => { + expect(estimateMediaTokensForMessage(undefined, true)).toBe(0); + }); + + it('returns 0 for a number', () => { + expect(estimateMediaTokensForMessage(42, false)).toBe(0); + }); + }); + + describe('empty and malformed arrays', () => { + it('returns 0 for an empty array', () => { + expect(estimateMediaTokensForMessage([], false)).toBe(0); + }); + + it('skips null entries', () => { + expect(estimateMediaTokensForMessage([null, undefined], false)).toBe(0); + }); + + it('skips entries without a string type', () => { + expect(estimateMediaTokensForMessage([{ type: 123 }, { text: 'hi' }], false)).toBe(0); + }); + + it('skips text-only blocks (not media)', () => { + expect(estimateMediaTokensForMessage([{ type: 'text', text: 'hi' }], false)).toBe(0); + }); + }); + + describe('image_url blocks', () => { + it('falls back to 1024 for a remote URL (non-data)', () => { + const content = [{ type: 'image_url', image_url: 'https://example.com/img.png' }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(1024); + }); + + it('falls back to 1024 when image_url is an object with non-data URL', () => { + const content = [{ type: 'image_url', image_url: { url: 'https://example.com/img.png' } }]; + expect(estimateMediaTokensForMessage(content, true)).toBe(1024); + }); + + it('falls back to 1024 when base64 data cannot be decoded', () => { + const content = [{ type: 'image_url', image_url: 'data:image/png;base64,SHORT' }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(1024); + }); + + it('estimates tokens from decoded dimensions (OpenAI path)', () => { + const content = [{ type: 'image_url', image_url: 'data:image/png;base64,VALID_PNG_LONG_DATA' }]; + const result = estimateMediaTokensForMessage(content, false); + expect(result).toBeGreaterThan(0); + expect(result).not.toBe(1024); + }); + + it('estimates tokens from decoded dimensions (Claude path)', () => { + const content = [{ type: 'image_url', image_url: { url: 'data:image/png;base64,VALID_PNG_LONG_DATA' } }]; + const result = estimateMediaTokensForMessage(content, true); + expect(result).toBeGreaterThan(0); + expect(result).not.toBe(1024); + }); + }); + + describe('image blocks (Anthropic format)', () => { + it('falls back to 1024 when source is not base64', () => { + const content = [{ type: 'image', source: { type: 'url', data: 'https://example.com' } }]; + expect(estimateMediaTokensForMessage(content, true)).toBe(1024); + }); + + it('falls back to 1024 when dimensions cannot be extracted', () => { + const content = [{ type: 'image', source: { type: 'base64', data: 'INVALID' } }]; + expect(estimateMediaTokensForMessage(content, true)).toBe(1024); + }); + + it('estimates tokens from valid base64 image data', () => { + const content = [{ type: 'image', source: { type: 'base64', data: 'VALID_PNG' } }]; + const result = estimateMediaTokensForMessage(content, true); + expect(result).toBeGreaterThan(0); + expect(result).not.toBe(1024); + }); + }); + + describe('image_file blocks', () => { + it('falls back to 1024 (no base64 extraction path)', () => { + const content = [{ type: 'image_file', file_id: 'file-abc' }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(1024); + }); + }); + + describe('document blocks - LangChain format (source_type)', () => { + it('counts tokens for text source_type with getTokenCount', () => { + const content = [{ + type: 'document', + source_type: 'text', + text: 'a'.repeat(400), + }]; + expect(estimateMediaTokensForMessage(content, false, fakeTokenCount)).toBe(100); + }); + + it('falls back to length/4 without getTokenCount', () => { + const content = [{ + type: 'document', + source_type: 'text', + text: 'a'.repeat(400), + }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(100); + }); + + it('estimates PDF pages for base64 source_type with application/pdf mime', () => { + const pdfData = 'x'.repeat(150_000); + const content = [{ + type: 'document', + source_type: 'base64', + data: pdfData, + mime_type: 'application/pdf', + }]; + const result = estimateMediaTokensForMessage(content, false); + expect(result).toBe(2 * 1500); + }); + + it('uses Claude PDF rate when isClaude is true', () => { + const pdfData = 'x'.repeat(150_000); + const content = [{ + type: 'document', + source_type: 'base64', + data: pdfData, + mime_type: 'application/pdf', + }]; + const result = estimateMediaTokensForMessage(content, true); + expect(result).toBe(2 * 2000); + }); + + it('defaults to PDF estimation for empty mime_type', () => { + const pdfData = 'x'.repeat(10); + const content = [{ + type: 'document', + source_type: 'base64', + data: pdfData, + mime_type: '', + }]; + const result = estimateMediaTokensForMessage(content, false); + expect(result).toBe(1 * 1500); + }); + + it('handles image/* mime inside base64 source_type', () => { + const content = [{ + type: 'document', + source_type: 'base64', + data: 'VALID_PNG', + mime_type: 'image/png', + }]; + const result = estimateMediaTokensForMessage(content, true); + expect(result).toBeGreaterThan(0); + expect(result).not.toBe(1024); + }); + + it('falls back to 1024 for undecodable image in base64 source_type', () => { + const content = [{ + type: 'document', + source_type: 'base64', + data: 'BAD_DATA', + mime_type: 'image/jpeg', + }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(1024); + }); + + it('falls back to URL_DOCUMENT_FALLBACK_TOKENS for unrecognized source_type', () => { + const content = [{ type: 'document', source_type: 'url' }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(2000); + }); + }); + + describe('document blocks - Anthropic format (source object)', () => { + it('counts tokens for text source type with getTokenCount', () => { + const content = [{ + type: 'document', + source: { type: 'text', data: 'a'.repeat(800) }, + }]; + expect(estimateMediaTokensForMessage(content, true, fakeTokenCount)).toBe(200); + }); + + it('falls back to length/4 for text source without getTokenCount', () => { + const content = [{ + type: 'document', + source: { type: 'text', data: 'a'.repeat(800) }, + }]; + expect(estimateMediaTokensForMessage(content, true)).toBe(200); + }); + + it('estimates PDF pages for base64 source with application/pdf', () => { + const pdfData = 'x'.repeat(225_000); + const content = [{ + type: 'document', + source: { type: 'base64', data: pdfData, media_type: 'application/pdf' }, + }]; + const result = estimateMediaTokensForMessage(content, true); + expect(result).toBe(3 * 2000); + }); + + it('returns URL fallback for url source type', () => { + const content = [{ + type: 'document', + source: { type: 'url' }, + }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(2000); + }); + + it('handles content source type with nested images', () => { + const content = [{ + type: 'document', + source: { + type: 'content', + content: [ + { type: 'image', source: { type: 'base64', data: 'VALID_PNG' } }, + { type: 'image', source: { type: 'base64', data: 'UNDECODABLE' } }, + ], + }, + }]; + const result = estimateMediaTokensForMessage(content, true); + expect(result).toBeGreaterThan(1024); + }); + + it('falls back to URL_DOCUMENT_FALLBACK_TOKENS when source has unknown type', () => { + const content = [{ type: 'document', source: { type: 'unknown_format' } }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(2000); + }); + }); + + describe('file blocks', () => { + it('uses same logic as document for file type blocks', () => { + const content = [{ + type: 'file', + source_type: 'text', + text: 'a'.repeat(120), + }]; + expect(estimateMediaTokensForMessage(content, false, fakeTokenCount)).toBe(30); + }); + + it('falls back to URL_DOCUMENT_FALLBACK_TOKENS for file without source info', () => { + const content = [{ type: 'file' }]; + expect(estimateMediaTokensForMessage(content, false)).toBe(2000); + }); + }); + + describe('mixed content arrays', () => { + it('sums tokens across multiple media blocks', () => { + const content = [ + { type: 'text', text: 'hello' }, + { type: 'image_url', image_url: 'https://example.com/img.png' }, + { type: 'image_file', file_id: 'f1' }, + { type: 'document', source: { type: 'url' } }, + ]; + const result = estimateMediaTokensForMessage(content, false); + expect(result).toBe(1024 + 1024 + 2000); + }); + }); +}); diff --git a/packages/api/src/agents/__tests__/initialize.test.ts b/packages/api/src/agents/__tests__/initialize.test.ts index 01310a09c4..f9982a6e46 100644 --- a/packages/api/src/agents/__tests__/initialize.test.ts +++ b/packages/api/src/agents/__tests__/initialize.test.ts @@ -190,7 +190,7 @@ describe('initializeAgent — maxContextTokens', () => { db, ); - const expected = Math.round((modelDefault - maxOutputTokens) * 0.9); + const expected = Math.round((modelDefault - maxOutputTokens) * 0.95); expect(result.maxContextTokens).toBe(expected); }); @@ -222,7 +222,7 @@ describe('initializeAgent — maxContextTokens', () => { // optionalChainWithEmptyCheck(0, 200000, 18000) returns 0 (not null/undefined), // then Number(0) || 18000 = 18000 (the fallback default). expect(result.maxContextTokens).not.toBe(0); - const expected = Math.round((18000 - maxOutputTokens) * 0.9); + const expected = Math.round((18000 - maxOutputTokens) * 0.95); expect(result.maxContextTokens).toBe(expected); }); @@ -278,7 +278,59 @@ describe('initializeAgent — maxContextTokens', () => { db, ); - // Should NOT be overridden to Math.round((128000 - 4096) * 0.9) = 111,514 + // Should NOT be overridden to Math.round((128000 - 4096) * 0.95) = 117,709 expect(result.maxContextTokens).toBe(userValue); }); + + it('sets baseContextTokens to agentMaxContextNum minus maxOutputTokensNum', async () => { + const modelDefault = 200000; + const maxOutputTokens = 4096; + const { agent, req, res, loadTools, db } = createMocks({ + maxContextTokens: undefined, + modelDefault, + maxOutputTokens, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { endpoint: EModelEndpoint.agents }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + expect(result.baseContextTokens).toBe(modelDefault - maxOutputTokens); + }); + + it('clamps maxContextTokens to at least 1024 for tiny models', async () => { + const modelDefault = 1100; + const maxOutputTokens = 1050; + const { agent, req, res, loadTools, db } = createMocks({ + maxContextTokens: undefined, + modelDefault, + maxOutputTokens, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { endpoint: EModelEndpoint.agents }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + // baseContextTokens = 1100 - 1050 = 50, formula would give ~47.5 rounded + // but Math.max(1024, ...) clamps it + expect(result.maxContextTokens).toBe(1024); + }); }); diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts new file mode 100644 index 0000000000..2bc0da253a --- /dev/null +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -0,0 +1,299 @@ +import type { SummarizationConfig } from 'librechat-data-provider'; +import { createRun } from '~/agents/run'; + +// Mock winston logger +jest.mock('winston', () => ({ + createLogger: jest.fn(() => ({ + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + info: jest.fn(), + })), + format: { combine: jest.fn(), colorize: jest.fn(), simple: jest.fn() }, + transports: { Console: jest.fn() }, +})); + +// Mock env utilities so header resolution doesn't fail +jest.mock('~/utils/env', () => ({ + resolveHeaders: jest.fn((opts: { headers: unknown }) => opts?.headers ?? {}), + createSafeUser: jest.fn(() => ({})), +})); + +// Mock Run.create to capture the graphConfig it receives +jest.mock('@librechat/agents', () => { + const actual = jest.requireActual('@librechat/agents'); + return { + ...actual, + Run: { + create: jest.fn().mockResolvedValue({ + processStream: jest.fn().mockResolvedValue(undefined), + }), + }, + }; +}); + +import { Run } from '@librechat/agents'; + +/** Minimal RunAgent factory */ +function makeAgent( + overrides?: Record, +): Record & { id: string; provider: string; model: string } { + return { + id: 'agent_1', + provider: 'openAI', + endpoint: 'openAI', + model: 'gpt-4o', + tools: [], + model_parameters: { model: 'gpt-4o' }, + maxContextTokens: 100_000, + toolContextMap: {}, + ...overrides, + }; +} + +/** Helper: call createRun and return the captured agentInputs array */ +async function callAndCapture( + opts: { + agents?: ReturnType[]; + summarizationConfig?: SummarizationConfig; + initialSummary?: { text: string; tokenCount: number }; + } = {}, +) { + const agents = opts.agents ?? [makeAgent()]; + const signal = new AbortController().signal; + + await createRun({ + agents: agents as never, + signal, + summarizationConfig: opts.summarizationConfig, + initialSummary: opts.initialSummary, + streaming: true, + streamUsage: true, + }); + + const createMock = Run.create as jest.Mock; + expect(createMock).toHaveBeenCalledTimes(1); + const callArgs = createMock.mock.calls[0][0]; + return callArgs.graphConfig.agents as Array>; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +// --------------------------------------------------------------------------- +// Suite 1: reserveRatio +// --------------------------------------------------------------------------- +describe('reserveRatio', () => { + it('applies ratio from config using baseContextTokens, capped at maxContextTokens', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ baseContextTokens: 200_000, maxContextTokens: 200_000 })], + summarizationConfig: { reserveRatio: 0.03, provider: 'anthropic', model: 'claude' }, + }); + // Math.round(200000 * 0.97) = 194000, min(200000, 194000) = 194000 + expect(agents[0].maxContextTokens).toBe(194_000); + }); + + it('never exceeds user-configured maxContextTokens even when ratio computes higher', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ baseContextTokens: 200_000, maxContextTokens: 50_000 })], + summarizationConfig: { reserveRatio: 0.03, provider: 'anthropic', model: 'claude' }, + }); + // Math.round(200000 * 0.97) = 194000, but min(50000, 194000) = 50000 + expect(agents[0].maxContextTokens).toBe(50_000); + }); + + it('falls back to maxContextTokens when ratio is not set', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ maxContextTokens: 100_000, baseContextTokens: 200_000 })], + summarizationConfig: { provider: 'anthropic', model: 'claude' }, + }); + expect(agents[0].maxContextTokens).toBe(100_000); + }); + + it('falls back to maxContextTokens when ratio is 0', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ maxContextTokens: 100_000, baseContextTokens: 200_000 })], + summarizationConfig: { reserveRatio: 0, provider: 'anthropic', model: 'claude' }, + }); + expect(agents[0].maxContextTokens).toBe(100_000); + }); + + it('falls back to maxContextTokens when ratio is 1', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ maxContextTokens: 100_000, baseContextTokens: 200_000 })], + summarizationConfig: { reserveRatio: 1, provider: 'anthropic', model: 'claude' }, + }); + expect(agents[0].maxContextTokens).toBe(100_000); + }); + + it('falls back to maxContextTokens when baseContextTokens is undefined', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ maxContextTokens: 100_000 })], + summarizationConfig: { reserveRatio: 0.05, provider: 'anthropic', model: 'claude' }, + }); + expect(agents[0].maxContextTokens).toBe(100_000); + }); + + it('clamps to 1024 minimum but still capped at maxContextTokens', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ baseContextTokens: 500, maxContextTokens: 2000 })], + summarizationConfig: { reserveRatio: 0.99, provider: 'anthropic', model: 'claude' }, + }); + // Math.round(500 * 0.01) = 5 → clamped to 1024, min(2000, 1024) = 1024 + expect(agents[0].maxContextTokens).toBe(1024); + }); +}); + +// --------------------------------------------------------------------------- +// Suite 2: maxSummaryTokens passthrough +// --------------------------------------------------------------------------- +describe('maxSummaryTokens passthrough', () => { + it('forwards global maxSummaryTokens value', async () => { + const agents = await callAndCapture({ + summarizationConfig: { + provider: 'anthropic', + model: 'claude', + maxSummaryTokens: 4096, + }, + }); + const config = agents[0].summarizationConfig as Record; + expect(config.maxSummaryTokens).toBe(4096); + }); +}); + +// --------------------------------------------------------------------------- +// Suite 3: summarizationEnabled resolution +// --------------------------------------------------------------------------- +describe('summarizationEnabled resolution', () => { + it('true with provider + model + enabled', async () => { + const agents = await callAndCapture({ + summarizationConfig: { + enabled: true, + provider: 'anthropic', + model: 'claude-3-haiku', + }, + }); + expect(agents[0].summarizationEnabled).toBe(true); + }); + + it('false when provider is empty string', async () => { + const agents = await callAndCapture({ + summarizationConfig: { + enabled: true, + provider: '', + model: 'claude-3-haiku', + }, + }); + expect(agents[0].summarizationEnabled).toBe(false); + }); + + it('false when enabled is explicitly false', async () => { + const agents = await callAndCapture({ + summarizationConfig: { + enabled: false, + provider: 'anthropic', + model: 'claude-3-haiku', + }, + }); + expect(agents[0].summarizationEnabled).toBe(false); + }); + + it('true with self-summarize default when summarizationConfig is undefined', async () => { + const agents = await callAndCapture({ + summarizationConfig: undefined, + }); + expect(agents[0].summarizationEnabled).toBe(true); + const config = agents[0].summarizationConfig as Record; + expect(config.provider).toBe('openAI'); + expect(config.model).toBe('gpt-4o'); + }); +}); + +// --------------------------------------------------------------------------- +// Suite 4: summarizationConfig field passthrough +// --------------------------------------------------------------------------- +describe('summarizationConfig field passthrough', () => { + it('all fields pass through to agentInputs', async () => { + const agents = await callAndCapture({ + summarizationConfig: { + enabled: true, + trigger: { type: 'token_count', value: 8000 }, + provider: 'anthropic', + model: 'claude-3-haiku', + parameters: { temperature: 0.2 }, + prompt: 'Summarize this conversation', + updatePrompt: 'Update the existing summary with new messages', + reserveRatio: 0.1, + maxSummaryTokens: 4096, + }, + }); + const config = agents[0].summarizationConfig as Record; + expect(config).toBeDefined(); + // `enabled` is not forwarded to the agent-level config — it is resolved + // into the separate `summarizationEnabled` boolean on the agent input. + expect(agents[0].summarizationEnabled).toBe(true); + expect(config.trigger).toEqual({ type: 'token_count', value: 8000 }); + expect(config.provider).toBe('anthropic'); + expect(config.model).toBe('claude-3-haiku'); + expect(config.parameters).toEqual({ temperature: 0.2 }); + expect(config.prompt).toBe('Summarize this conversation'); + expect(config.updatePrompt).toBe('Update the existing summary with new messages'); + expect(config.reserveRatio).toBe(0.1); + expect(config.maxSummaryTokens).toBe(4096); + }); + + it('uses self-summarize default when no config provided', async () => { + const agents = await callAndCapture({ + summarizationConfig: undefined, + }); + const config = agents[0].summarizationConfig as Record; + expect(config).toBeDefined(); + // `enabled` is resolved into `summarizationEnabled`, not forwarded on config + expect(agents[0].summarizationEnabled).toBe(true); + expect(config.provider).toBe('openAI'); + expect(config.model).toBe('gpt-4o'); + }); +}); + +// --------------------------------------------------------------------------- +// Suite 5: Multi-agent + per-agent overrides +// --------------------------------------------------------------------------- +describe('multi-agent + per-agent overrides', () => { + it('different agents get different effectiveMaxContextTokens', async () => { + const agents = await callAndCapture({ + agents: [ + makeAgent({ id: 'agent_1', baseContextTokens: 200_000, maxContextTokens: 100_000 }), + makeAgent({ id: 'agent_2', baseContextTokens: 100_000, maxContextTokens: 50_000 }), + ], + summarizationConfig: { + reserveRatio: 0.1, + provider: 'anthropic', + model: 'claude', + }, + }); + // agent_1: Math.round(200000 * 0.9) = 180000, but capped at user's maxContextTokens (100000) + expect(agents[0].maxContextTokens).toBe(100_000); + // agent_2: Math.round(100000 * 0.9) = 90000, but capped at user's maxContextTokens (50000) + expect(agents[1].maxContextTokens).toBe(50_000); + }); +}); + +// --------------------------------------------------------------------------- +// Suite 6: initialSummary passthrough +// --------------------------------------------------------------------------- +describe('initialSummary passthrough', () => { + it('forwarded to agent inputs', async () => { + const summary = { text: 'Previous conversation summary', tokenCount: 500 }; + const agents = await callAndCapture({ + initialSummary: summary, + summarizationConfig: { provider: 'anthropic', model: 'claude' }, + }); + expect(agents[0].initialSummary).toEqual(summary); + }); + + it('undefined when not provided', async () => { + const agents = await callAndCapture({}); + expect(agents[0].initialSummary).toBeUndefined(); + }); +}); diff --git a/packages/api/src/agents/__tests__/summarization.e2e.test.ts b/packages/api/src/agents/__tests__/summarization.e2e.test.ts new file mode 100644 index 0000000000..03ef2ca6d4 --- /dev/null +++ b/packages/api/src/agents/__tests__/summarization.e2e.test.ts @@ -0,0 +1,595 @@ +/** + * E2E Backend Integration Tests for Summarization + * + * Exercises the FULL LibreChat -> agents pipeline: + * LibreChat's createRun (@librechat/api) + * -> agents package Run.create (@librechat/agents) + * -> graph execution -> summarization node -> events + * + * Uses real AI providers, real formatAgentMessages, real token accounting. + * Tracks summaries both mid-run and between runs. + * + * Run from packages/api: + * npx jest summarization.e2e --no-coverage --testTimeout=180000 + * + * Requires real API keys in the environment (ANTHROPIC_API_KEY, OPENAI_API_KEY). + */ +import { + Providers, + Calculator, + GraphEvents, + ToolEndHandler, + ModelEndHandler, + createTokenCounter, + formatAgentMessages, + ChatModelStreamHandler, + createContentAggregator, +} from '@librechat/agents'; +import type { + SummarizeCompleteEvent, + MessageContentComplex, + SummaryContentBlock, + SummarizeStartEvent, + TokenCounter, + EventHandler, +} from '@librechat/agents'; +import { hydrateMissingIndexTokenCounts } from '~/utils'; +import { ioredisClient, keyvRedisClient } from '~/cache'; +import { createRun } from '~/agents'; + +afterAll(async () => { + await ioredisClient?.quit().catch(() => {}); + await keyvRedisClient?.disconnect().catch(() => {}); +}); + +// --------------------------------------------------------------------------- +// Shared test infrastructure +// --------------------------------------------------------------------------- + +interface Spies { + onMessageDelta: jest.Mock; + onRunStep: jest.Mock; + onSummarizeStart: jest.Mock; + onSummarizeDelta: jest.Mock; + onSummarizeComplete: jest.Mock; +} + +type PayloadMessage = { + role: string; + content: string | Array>; +}; + +function getSummaryText(summary: SummaryContentBlock): string { + if (Array.isArray(summary.content)) { + return summary.content + .map((b: MessageContentComplex) => ('text' in b ? (b as { text: string }).text : '')) + .join(''); + } + return ''; +} + +function createSpies(): Spies { + return { + onMessageDelta: jest.fn(), + onRunStep: jest.fn(), + onSummarizeStart: jest.fn(), + onSummarizeDelta: jest.fn(), + onSummarizeComplete: jest.fn(), + }; +} + +function buildHandlers( + collectedUsage: ConstructorParameters[0], + aggregateContent: (params: { event: string; data: unknown }) => void, + spies: Spies, +): Record { + return { + [GraphEvents.TOOL_END]: new ToolEndHandler(), + [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage), + [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(), + [GraphEvents.ON_RUN_STEP]: { + handle: (event: string, data: unknown) => { + spies.onRunStep(event, data); + aggregateContent({ event, data }); + }, + }, + [GraphEvents.ON_RUN_STEP_COMPLETED]: { + handle: (event: string, data: unknown) => { + aggregateContent({ event, data }); + }, + }, + [GraphEvents.ON_RUN_STEP_DELTA]: { + handle: (event: string, data: unknown) => { + aggregateContent({ event, data }); + }, + }, + [GraphEvents.ON_MESSAGE_DELTA]: { + handle: (event: string, data: unknown, metadata?: Record) => { + spies.onMessageDelta(event, data, metadata); + aggregateContent({ event, data }); + }, + }, + [GraphEvents.TOOL_START]: { + handle: () => {}, + }, + [GraphEvents.ON_SUMMARIZE_START]: { + handle: (_event: string, data: unknown) => { + spies.onSummarizeStart(data); + }, + }, + [GraphEvents.ON_SUMMARIZE_DELTA]: { + handle: (_event: string, data: unknown) => { + spies.onSummarizeDelta(data); + aggregateContent({ event: GraphEvents.ON_SUMMARIZE_DELTA, data }); + }, + }, + [GraphEvents.ON_SUMMARIZE_COMPLETE]: { + handle: (_event: string, data: unknown) => { + spies.onSummarizeComplete(data); + }, + }, + }; +} + +function getDefaultModel(provider: string): string { + switch (provider) { + case Providers.ANTHROPIC: + return 'claude-haiku-4-5-20251001'; + case Providers.OPENAI: + return 'gpt-4.1-mini'; + default: + return 'gpt-4.1-mini'; + } +} + +// --------------------------------------------------------------------------- +// Turn runner — mirrors AgentClient.chatCompletion() message flow +// --------------------------------------------------------------------------- + +interface RunFullTurnParams { + payload: PayloadMessage[]; + agentProvider: string; + summarizationProvider: string; + summarizationModel?: string; + maxContextTokens: number; + instructions: string; + spies: Spies; + tokenCounter: TokenCounter; + model?: string; +} + +async function runFullTurn({ + payload, + agentProvider, + summarizationProvider, + summarizationModel, + maxContextTokens, + instructions, + spies, + tokenCounter, + model, +}: RunFullTurnParams) { + const collectedUsage: ConstructorParameters[0] = []; + const { contentParts, aggregateContent } = createContentAggregator(); + + const formatted = formatAgentMessages(payload as never, {}); + const { messages: initialMessages, summary: initialSummary } = formatted; + let { indexTokenCountMap } = formatted; + + indexTokenCountMap = hydrateMissingIndexTokenCounts({ + messages: initialMessages, + indexTokenCountMap: indexTokenCountMap as Record, + tokenCounter, + }); + + const abortController = new AbortController(); + const agent = { + id: `test-agent-${agentProvider}`, + name: 'Test Agent', + provider: agentProvider, + instructions, + tools: [new Calculator()], + maxContextTokens, + model_parameters: { + model: model || getDefaultModel(agentProvider), + streaming: true, + streamUsage: true, + }, + }; + + const summarizationConfig = { + enabled: true, + provider: summarizationProvider, + model: summarizationModel || getDefaultModel(summarizationProvider), + prompt: + 'You are a summarization assistant. Summarize the following conversation messages concisely, preserving key facts, decisions, and context needed to continue the conversation. Do not include preamble -- output only the summary.', + }; + + const run = await createRun({ + agents: [agent] as never, + messages: initialMessages, + indexTokenCountMap, + initialSummary, + runId: `e2e-${Date.now()}`, + signal: abortController.signal, + customHandlers: buildHandlers(collectedUsage, aggregateContent, spies) as never, + summarizationConfig, + tokenCounter, + }); + + const streamConfig = { + configurable: { thread_id: `e2e-${Date.now()}` }, + recursionLimit: 100, + streamMode: 'values' as const, + version: 'v2' as const, + }; + let result: unknown; + let processError: Error | undefined; + try { + result = await run.processStream({ messages: initialMessages }, streamConfig); + } catch (err) { + processError = err as Error; + } + const runMessages = run.getRunMessages() || []; + + return { + result, + processError, + runMessages, + collectedUsage, + contentParts, + indexTokenCountMap, + }; +} + +function getLastContent(runMessages: Array<{ content: string | unknown }>): string { + const last = runMessages[runMessages.length - 1]; + if (!last) { + return ''; + } + return typeof last.content === 'string' ? last.content : JSON.stringify(last.content); +} + +// --------------------------------------------------------------------------- +// Anthropic Tests +// --------------------------------------------------------------------------- + +const hasAnthropic = + process.env.ANTHROPIC_API_KEY != null && process.env.ANTHROPIC_API_KEY !== 'test'; +(hasAnthropic ? describe : describe.skip)('Anthropic Summarization E2E (LibreChat)', () => { + jest.setTimeout(180_000); + + const instructions = + 'You are an expert math tutor. You MUST use the calculator tool for ALL computations. Keep answers to 1-2 sentences.'; + + test('multi-turn triggers summarization, summary persists across runs', async () => { + const spies = createSpies(); + const tokenCounter = await createTokenCounter(); + const conversationPayload: PayloadMessage[] = []; + + const addTurn = async (userMsg: string, maxTokens: number) => { + conversationPayload.push({ role: 'user', content: userMsg }); + const result = await runFullTurn({ + payload: conversationPayload, + agentProvider: Providers.ANTHROPIC, + summarizationProvider: Providers.ANTHROPIC, + summarizationModel: 'claude-haiku-4-5-20251001', + maxContextTokens: maxTokens, + instructions, + spies, + tokenCounter, + }); + conversationPayload.push({ role: 'assistant', content: getLastContent(result.runMessages) }); + return result; + }; + + await addTurn('What is 12345 * 6789? Use the calculator.', 2000); + await addTurn( + 'Now divide that result by 137. Then multiply by 42. Calculator for each step.', + 2000, + ); + await addTurn( + 'Compute step by step: 1) 9876543 - 1234567 2) sqrt of result 3) Add 100. Calculator for each.', + 1500, + ); + await addTurn('What is 2^20? Calculator. Then list everything we calculated so far.', 800); + + if (spies.onSummarizeStart.mock.calls.length === 0) { + await addTurn('Calculate 355 / 113. Calculator.', 600); + } + if (spies.onSummarizeStart.mock.calls.length === 0) { + await addTurn('What is 999 * 999? Calculator.', 400); + } + + const startCalls = spies.onSummarizeStart.mock.calls.length; + const completeCalls = spies.onSummarizeComplete.mock.calls.length; + + expect(startCalls).toBeGreaterThanOrEqual(1); + expect(completeCalls).toBeGreaterThanOrEqual(1); + + const startPayload = spies.onSummarizeStart.mock.calls[0][0] as SummarizeStartEvent; + expect(startPayload.agentId).toBeDefined(); + expect(startPayload.provider).toBeDefined(); + expect(startPayload.messagesToRefineCount).toBeGreaterThan(0); + expect(startPayload.summaryVersion).toBeGreaterThanOrEqual(1); + + const completePayload = spies.onSummarizeComplete.mock.calls[0][0] as SummarizeCompleteEvent; + expect(completePayload.summary).toBeDefined(); + expect(getSummaryText(completePayload.summary!).length).toBeGreaterThan(10); + expect(completePayload.summary!.tokenCount).toBeGreaterThan(0); + expect(completePayload.summary!.tokenCount!).toBeLessThan(2000); + expect(completePayload.summary!.provider).toBeDefined(); + expect(completePayload.summary!.createdAt).toBeDefined(); + expect(completePayload.summary!.summaryVersion).toBeGreaterThanOrEqual(1); + + // --- Cross-run: persist summary -> formatAgentMessages -> new run --- + const summaryBlock = completePayload.summary!; + const crossRunPayload: PayloadMessage[] = [ + { + role: 'assistant', + content: [ + { + type: 'summary', + content: [{ type: 'text', text: getSummaryText(summaryBlock) }], + tokenCount: summaryBlock.tokenCount, + }, + ], + }, + conversationPayload[conversationPayload.length - 2], + conversationPayload[conversationPayload.length - 1], + { + role: 'user', + content: 'What was the first calculation we did? Verify with calculator.', + }, + ]; + + spies.onSummarizeStart.mockClear(); + spies.onSummarizeComplete.mockClear(); + + const crossRun = await runFullTurn({ + payload: crossRunPayload, + agentProvider: Providers.ANTHROPIC, + summarizationProvider: Providers.ANTHROPIC, + summarizationModel: 'claude-haiku-4-5-20251001', + maxContextTokens: 2000, + instructions, + spies, + tokenCounter, + }); + + console.log( + ` Cross-run: messages=${crossRun.runMessages.length}, content=${crossRun.contentParts.length}, deltas=${spies.onMessageDelta.mock.calls.length}`, + ); + // Content aggregator should have received response deltas even if getRunMessages is empty + expect(crossRun.contentParts.length + spies.onMessageDelta.mock.calls.length).toBeGreaterThan( + 0, + ); + }); + + test('tight context (maxContextTokens=200) does not infinite-loop', async () => { + const spies = createSpies(); + const tokenCounter = await createTokenCounter(); + const conversationPayload: PayloadMessage[] = []; + + conversationPayload.push({ role: 'user', content: 'What is 42 * 58? Calculator.' }); + const t1 = await runFullTurn({ + payload: conversationPayload, + agentProvider: Providers.ANTHROPIC, + summarizationProvider: Providers.ANTHROPIC, + summarizationModel: 'claude-haiku-4-5-20251001', + maxContextTokens: 2000, + instructions, + spies, + tokenCounter, + }); + conversationPayload.push({ role: 'assistant', content: getLastContent(t1.runMessages) }); + + conversationPayload.push({ role: 'user', content: 'Now compute 2436 + 1337. Calculator.' }); + const t2 = await runFullTurn({ + payload: conversationPayload, + agentProvider: Providers.ANTHROPIC, + summarizationProvider: Providers.ANTHROPIC, + summarizationModel: 'claude-haiku-4-5-20251001', + maxContextTokens: 2000, + instructions, + spies, + tokenCounter, + }); + conversationPayload.push({ role: 'assistant', content: getLastContent(t2.runMessages) }); + + conversationPayload.push({ role: 'user', content: 'What is 100 / 4? Calculator.' }); + + let error: Error | undefined; + try { + await runFullTurn({ + payload: conversationPayload, + agentProvider: Providers.ANTHROPIC, + summarizationProvider: Providers.ANTHROPIC, + summarizationModel: 'claude-haiku-4-5-20251001', + maxContextTokens: 200, + instructions, + spies, + tokenCounter, + }); + } catch (err) { + error = err as Error; + } + + // The key guarantee: the system terminates — no true infinite loop. + // With very tight context, the graph may either: + // 1. Complete normally (model responds within budget) + // 2. Hit recursion limit (bounded tool-call cycles) + // 3. Error with empty_messages (context too small for any message) + // All are valid termination modes. + if (error) { + const isCleanTermination = + error.message.includes('Recursion limit') || error.message.includes('empty_messages'); + + expect(isCleanTermination).toBe(true); + } + + // Summarization may or may not fire depending on whether the budget + // allows any messages before the graph terminates. With 200 tokens + // and instructions at ~100 tokens, there may be no room for history, + // which correctly skips summarization. + + console.log( + ` Tight context: summarize=${spies.onSummarizeStart.mock.calls.length}, error=${error?.message?.substring(0, 80) ?? 'none'}`, + ); + }); +}); + +// --------------------------------------------------------------------------- +// OpenAI Tests +// --------------------------------------------------------------------------- + +const hasOpenAI = process.env.OPENAI_API_KEY != null && process.env.OPENAI_API_KEY !== 'test'; +(hasOpenAI ? describe : describe.skip)('OpenAI Summarization E2E (LibreChat)', () => { + jest.setTimeout(180_000); + + const instructions = + 'You are a helpful math tutor. Use the calculator tool for ALL computations. Keep responses concise.'; + + test('multi-turn with cross-run summary continuity', async () => { + const spies = createSpies(); + const tokenCounter = await createTokenCounter(); + const conversationPayload: PayloadMessage[] = []; + + const addTurn = async (userMsg: string, maxTokens: number) => { + conversationPayload.push({ role: 'user', content: userMsg }); + const result = await runFullTurn({ + payload: conversationPayload, + agentProvider: Providers.OPENAI, + summarizationProvider: Providers.OPENAI, + summarizationModel: 'gpt-4.1-mini', + maxContextTokens: maxTokens, + instructions, + spies, + tokenCounter, + }); + conversationPayload.push({ role: 'assistant', content: getLastContent(result.runMessages) }); + return result; + }; + + await addTurn('What is 1234 * 5678? Calculator.', 2000); + await addTurn('Compute sqrt(7006652) with calculator.', 1500); + await addTurn('Calculate 99*101 and 2^15. Calculator for each.', 1200); + await addTurn('What is 314159 * 271828? Calculator. Remind me of all prior results.', 800); + + if (spies.onSummarizeStart.mock.calls.length === 0) { + await addTurn('Calculate 999999 / 7. Calculator.', 600); + } + if (spies.onSummarizeStart.mock.calls.length === 0) { + await addTurn('What is 42 + 58? Calculator.', 400); + } + if (spies.onSummarizeStart.mock.calls.length === 0) { + await addTurn('Calculate 7 * 13. Calculator.', 300); + } + if (spies.onSummarizeStart.mock.calls.length === 0) { + await addTurn('What is 100 - 37? Calculator.', 200); + } + + expect(spies.onSummarizeStart.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(spies.onSummarizeComplete.mock.calls.length).toBeGreaterThanOrEqual(1); + + const complete = spies.onSummarizeComplete.mock.calls[0][0] as SummarizeCompleteEvent; + expect(getSummaryText(complete.summary!).length).toBeGreaterThan(10); + expect(complete.summary!.tokenCount).toBeGreaterThan(0); + expect(complete.summary!.summaryVersion).toBeGreaterThanOrEqual(1); + expect(complete.summary!.provider).toBe(Providers.OPENAI); + + const summaryBlock = complete.summary!; + const crossRunPayload: PayloadMessage[] = [ + { + role: 'assistant', + content: [ + { + type: 'summary', + content: [{ type: 'text', text: getSummaryText(summaryBlock) }], + tokenCount: summaryBlock.tokenCount, + }, + ], + }, + conversationPayload[conversationPayload.length - 2], + conversationPayload[conversationPayload.length - 1], + { role: 'user', content: 'What was the first number we calculated? Verify with calculator.' }, + ]; + + spies.onSummarizeStart.mockClear(); + spies.onSummarizeComplete.mockClear(); + + const crossRun = await runFullTurn({ + payload: crossRunPayload, + agentProvider: Providers.OPENAI, + summarizationProvider: Providers.OPENAI, + summarizationModel: 'gpt-4.1-mini', + maxContextTokens: 2000, + instructions, + spies, + tokenCounter, + }); + + console.log( + ` Cross-run: messages=${crossRun.runMessages.length}, content=${crossRun.contentParts.length}, deltas=${spies.onMessageDelta.mock.calls.length}`, + ); + expect(crossRun.contentParts.length + spies.onMessageDelta.mock.calls.length).toBeGreaterThan( + 0, + ); + }); +}); + +// --------------------------------------------------------------------------- +// Cross-provider: Anthropic agent, OpenAI summarizer +// --------------------------------------------------------------------------- + +const hasBothProviders = hasAnthropic && hasOpenAI; +(hasBothProviders ? describe : describe.skip)( + 'Cross-provider Summarization E2E (LibreChat)', + () => { + jest.setTimeout(180_000); + + const instructions = + 'You are a math assistant. Use the calculator for every computation. Be brief.'; + + test('Anthropic agent with OpenAI summarizer', async () => { + const spies = createSpies(); + const tokenCounter = await createTokenCounter(); + const conversationPayload: PayloadMessage[] = []; + + const addTurn = async (userMsg: string, maxTokens: number) => { + conversationPayload.push({ role: 'user', content: userMsg }); + const result = await runFullTurn({ + payload: conversationPayload, + agentProvider: Providers.ANTHROPIC, + summarizationProvider: Providers.OPENAI, + summarizationModel: 'gpt-4.1-mini', + maxContextTokens: maxTokens, + instructions, + spies, + tokenCounter, + }); + conversationPayload.push({ + role: 'assistant', + content: getLastContent(result.runMessages), + }); + return result; + }; + + await addTurn('Compute 54321 * 12345 using calculator.', 2000); + await addTurn('Now calculate 670592745 / 99991. Calculator.', 1500); + await addTurn('What is sqrt(670592745)? Calculator.', 1000); + await addTurn('Compute 2^32 with calculator. List all prior results.', 600); + + if (spies.onSummarizeStart.mock.calls.length === 0) { + await addTurn('13 * 17 * 19 = ? Calculator.', 400); + } + + expect(spies.onSummarizeComplete.mock.calls.length).toBeGreaterThanOrEqual(1); + const complete = spies.onSummarizeComplete.mock.calls[0][0] as SummarizeCompleteEvent; + + expect(complete.summary!.provider).toBe(Providers.OPENAI); + expect(complete.summary!.model).toBe('gpt-4.1-mini'); + expect(getSummaryText(complete.summary!).length).toBeGreaterThan(10); + }); + }, +); diff --git a/packages/api/src/agents/client.ts b/packages/api/src/agents/client.ts index fd5d50f211..c84230572f 100644 --- a/packages/api/src/agents/client.ts +++ b/packages/api/src/agents/client.ts @@ -1,6 +1,12 @@ import { logger } from '@librechat/data-schemas'; -import { isAgentsEndpoint } from 'librechat-data-provider'; -import { labelContentByAgent, getTokenCountForMessage } from '@librechat/agents'; +import { ContentTypes, isAgentsEndpoint } from 'librechat-data-provider'; +import { + labelContentByAgent, + extractImageDimensions, + getTokenCountForMessage, + estimateOpenAIImageTokens, + estimateAnthropicImageTokens, +} from '@librechat/agents'; import type { MessageContentComplex } from '@librechat/agents'; import type { Agent, TMessage } from 'librechat-data-provider'; import type { BaseMessage } from '@langchain/core/messages'; @@ -27,10 +33,247 @@ export function payloadParser({ req, endpoint }: { req: ServerRequest; endpoint: return req.body?.endpointOption?.model_parameters; } +/** + * Anthropic's API consistently reports ~10% more tokens than the local + * claude tokenizer due to internal message framing and content encoding. + * Verified empirically across content types via the count_tokens endpoint. + */ +export const CLAUDE_TOKEN_CORRECTION = 1.1; +const IMAGE_TOKEN_SAFETY_MARGIN = 1.05; +const BASE64_BYTES_PER_PDF_PAGE = 75_000; +const PDF_TOKENS_PER_PAGE_CLAUDE = 2000; +const PDF_TOKENS_PER_PAGE_OPENAI = 1500; +const URL_DOCUMENT_FALLBACK_TOKENS = 2000; + +type ContentBlock = { + type?: string; + image_url?: string | { url?: string }; + source?: { type?: string; data?: string; media_type?: string; content?: unknown[] }; + source_type?: string; + mime_type?: string; + data?: string; + text?: string; + tool_call?: { name?: string; args?: string; output?: string }; +}; + +function estimateImageDataTokens(data: string, isClaude: boolean): number { + const dims = extractImageDimensions(data); + if (dims == null) { + return 1024; + } + const raw = isClaude + ? estimateAnthropicImageTokens(dims.width, dims.height) + : estimateOpenAIImageTokens(dims.width, dims.height); + return Math.ceil(raw * IMAGE_TOKEN_SAFETY_MARGIN); +} + +function estimateImageBlockTokens(block: ContentBlock, isClaude: boolean): number { + let base64Data: string | undefined; + if (block.type === 'image_url') { + const url = typeof block.image_url === 'string' ? block.image_url : block.image_url?.url; + if (typeof url === 'string' && url.startsWith('data:')) { + base64Data = url; + } + } else if (block.type === 'image') { + if (block.source?.type === 'base64' && typeof block.source.data === 'string') { + base64Data = block.source.data; + } + } + if (base64Data == null) { + return 1024; + } + return estimateImageDataTokens(base64Data, isClaude); +} + +function estimateDocumentBlockTokens( + block: ContentBlock, + isClaude: boolean, + countTokens?: (text: string) => number, +): number { + const pdfPerPage = isClaude ? PDF_TOKENS_PER_PAGE_CLAUDE : PDF_TOKENS_PER_PAGE_OPENAI; + + if (typeof block.source_type === 'string') { + if (block.source_type === 'text' && typeof block.text === 'string') { + return countTokens != null ? countTokens(block.text) : Math.ceil(block.text.length / 4); + } + if (block.source_type === 'base64' && typeof block.data === 'string') { + const mime = (block.mime_type ?? '').split(';')[0]; + if (mime === 'application/pdf' || mime === '') { + return Math.max(1, Math.ceil(block.data.length / BASE64_BYTES_PER_PDF_PAGE)) * pdfPerPage; + } + if (mime.startsWith('image/')) { + return estimateImageDataTokens(block.data, isClaude); + } + return countTokens != null ? countTokens(block.data) : Math.ceil(block.data.length / 4); + } + return URL_DOCUMENT_FALLBACK_TOKENS; + } + + if (block.source != null) { + if (block.source.type === 'text' && typeof block.source.data === 'string') { + return countTokens != null + ? countTokens(block.source.data) + : Math.ceil(block.source.data.length / 4); + } + if (block.source.type === 'base64' && typeof block.source.data === 'string') { + const mime = (block.source.media_type ?? '').split(';')[0]; + if (mime === 'application/pdf' || mime === '') { + const pages = Math.max(1, Math.ceil(block.source.data.length / BASE64_BYTES_PER_PDF_PAGE)); + return pages * pdfPerPage; + } + if (mime.startsWith('image/')) { + return estimateImageDataTokens(block.source.data, isClaude); + } + return countTokens != null + ? countTokens(block.source.data) + : Math.ceil(block.source.data.length / 4); + } + if (block.source.type === 'url') { + return URL_DOCUMENT_FALLBACK_TOKENS; + } + if (block.source.type === 'content' && Array.isArray(block.source.content)) { + let tokens = 0; + for (const inner of block.source.content) { + const innerBlock = inner as ContentBlock | null; + if ( + innerBlock?.type === 'image' && + innerBlock.source?.type === 'base64' && + typeof innerBlock.source.data === 'string' + ) { + tokens += estimateImageDataTokens(innerBlock.source.data, isClaude); + } + } + return tokens; + } + } + + return URL_DOCUMENT_FALLBACK_TOKENS; +} + +/** + * Estimates token cost for image and document blocks in a message's + * content array. Covers: image_url, image, image_file, document, file. + */ +export function estimateMediaTokensForMessage( + content: unknown, + isClaude: boolean, + getTokenCount?: (text: string) => number, +): number { + if (!Array.isArray(content)) { + return 0; + } + let tokens = 0; + for (const block of content as ContentBlock[]) { + if (block == null || typeof block !== 'object' || typeof block.type !== 'string') { + continue; + } + const type = block.type; + if (type === 'image_url' || type === 'image' || type === 'image_file') { + tokens += estimateImageBlockTokens(block, isClaude); + continue; + } + if (type === 'document' || type === 'file') { + tokens += estimateDocumentBlockTokens(block, isClaude, getTokenCount); + } + } + return tokens; +} + +/** + * Single-pass token counter for formatted messages (plain objects with role/content/name). + * Handles text, tool_call, image, and document content types in one iteration, + * then applies Claude correction when applicable. + */ +export function countFormattedMessageTokens( + message: Partial>, + encoding: Parameters[1], +): number { + const countTokens = (text: string) => Tokenizer.getTokenCount(text, encoding); + const isClaude = encoding === 'claude'; + + let numTokens = 3; + + const processValue = (value: unknown): void => { + if (Array.isArray(value)) { + for (const item of value) { + if (item == null || typeof item !== 'object') { + continue; + } + const block = item as ContentBlock; + const type = block.type; + if (typeof type !== 'string') { + continue; + } + + if (type === ContentTypes.THINK || type === ContentTypes.ERROR) { + continue; + } + + if ( + type === ContentTypes.IMAGE_URL || + type === 'image' || + type === ContentTypes.IMAGE_FILE + ) { + numTokens += estimateImageBlockTokens(block, isClaude); + continue; + } + + if (type === 'document' || type === 'file') { + numTokens += estimateDocumentBlockTokens(block, isClaude, countTokens); + continue; + } + + if (type === ContentTypes.TOOL_CALL && block.tool_call != null) { + const { name, args, output } = block.tool_call; + if (typeof name === 'string' && name) { + numTokens += countTokens(name); + } + if (typeof args === 'string' && args) { + numTokens += countTokens(args); + } + if (typeof output === 'string' && output) { + numTokens += countTokens(output); + } + continue; + } + + const nestedValue = (item as Record)[type]; + if (nestedValue != null) { + processValue(nestedValue); + } + } + return; + } + + if (typeof value === 'string') { + numTokens += countTokens(value); + } else if (typeof value === 'number') { + numTokens += countTokens(value.toString()); + } else if (typeof value === 'boolean') { + numTokens += countTokens(value.toString()); + } + }; + + for (const [key, value] of Object.entries(message)) { + processValue(value); + if (key === 'name') { + numTokens += 1; + } + } + + return isClaude ? Math.ceil(numTokens * CLAUDE_TOKEN_CORRECTION) : numTokens; +} + export function createTokenCounter(encoding: Parameters[1]) { + const isClaude = encoding === 'claude'; + const countTokens = (text: string) => Tokenizer.getTokenCount(text, encoding); return function (message: BaseMessage) { - const countTokens = (text: string) => Tokenizer.getTokenCount(text, encoding); - return getTokenCountForMessage(message, countTokens); + const count = getTokenCountForMessage( + message, + countTokens, + encoding as 'claude' | 'o200k_base', + ); + return isClaude ? Math.ceil(count * CLAUDE_TOKEN_CORRECTION) : count; }; } diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index d5bfca5aba..81bc89cac4 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -33,6 +33,13 @@ import { getProviderConfig } from '~/endpoints'; import { primeResources } from './resources'; import type { TFilterFilesByAgentAccess } from './resources'; +/** + * Fraction of context budget reserved as headroom when no explicit maxContextTokens is set. + * Reduced from 0.10 to 0.05 alongside the introduction of summarization, which actively + * manages overflow. `createRun` can further override this via `SummarizationConfig.reserveRatio`. + */ +const DEFAULT_RESERVE_RATIO = 0.05; + /** * Extended agent type with additional fields needed after initialization */ @@ -41,6 +48,8 @@ export type InitializedAgent = Agent & { attachments: IMongoFile[]; toolContextMap: Record; maxContextTokens: number; + /** Pre-ratio context budget (agentMaxContextNum - maxOutputTokensNum). Used by createRun to apply a configurable reserve ratio. */ + baseContextTokens?: number; useLegacyContent: boolean; resendFiles: boolean; tool_resources?: AgentToolResources; @@ -55,6 +64,8 @@ export type InitializedAgent = Agent & { hasDeferredTools?: boolean; /** Whether the actions capability is enabled (resolved during tool loading) */ actionsEnabled?: boolean; + /** Maximum characters allowed in a single tool result before truncation. */ + maxToolResultChars?: number; }; /** @@ -311,7 +322,7 @@ export async function initializeAgent( actionsEnabled: undefined, }; - const { getOptions, overrideProvider } = getProviderConfig({ + const { getOptions, overrideProvider, customEndpointConfig } = getProviderConfig({ provider, appConfig: req.config, }); @@ -405,11 +416,25 @@ export async function initializeAgent( const agentMaxContextNum = Number(agentMaxContextTokens) || 18000; const maxOutputTokensNum = Number(maxOutputTokens) || 0; + const baseContextTokens = Math.max(0, agentMaxContextNum - maxOutputTokensNum); const finalAttachments: IMongoFile[] = (primedAttachments ?? []) .filter((a): a is TFile => a != null) .map((a) => a as unknown as IMongoFile); + const endpointConfigs = req.config?.endpoints; + const providerConfig = + customEndpointConfig ?? endpointConfigs?.[agent.provider as keyof typeof endpointConfigs]; + const providerMaxToolResultChars = + providerConfig != null && + typeof providerConfig === 'object' && + !Array.isArray(providerConfig) && + 'maxToolResultChars' in providerConfig + ? (providerConfig.maxToolResultChars as number | undefined) + : undefined; + const maxToolResultCharsResolved = + providerMaxToolResultChars ?? endpointConfigs?.all?.maxToolResultChars; + const initializedAgent: InitializedAgent = { ...agent, resendFiles, @@ -419,14 +444,16 @@ export async function initializeAgent( toolDefinitions, hasDeferredTools, actionsEnabled, + baseContextTokens, attachments: finalAttachments, toolContextMap: toolContextMap ?? {}, useLegacyContent: !!options.useLegacyContent, tools: (tools ?? []) as GenericTool[] & string[], + maxToolResultChars: maxToolResultCharsResolved, maxContextTokens: maxContextTokens != null && maxContextTokens > 0 ? maxContextTokens - : Math.round((agentMaxContextNum - maxOutputTokensNum) * 0.9), + : Math.max(1024, Math.round(baseContextTokens * (1 - DEFAULT_RESERVE_RATIO))), }; return initializedAgent; diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 189ef59469..b6b5e6a14d 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -1,8 +1,9 @@ import { Run, Providers, Constants } from '@librechat/agents'; import { providerEndpointMap, KnownEndpoints } from 'librechat-data-provider'; -import type { BaseMessage } from '@langchain/core/messages'; import type { + SummarizationConfig as AgentSummarizationConfig, MultiAgentGraphConfig, + ContextPruningConfig, OpenAIClientOptions, StandardGraphConfig, LCToolRegistry, @@ -12,8 +13,9 @@ import type { IState, LCTool, } from '@librechat/agents'; +import type { Agent, SummarizationConfig } from 'librechat-data-provider'; +import type { BaseMessage } from '@langchain/core/messages'; import type { IUser } from '@librechat/data-schemas'; -import type { Agent } from 'librechat-data-provider'; import type * as t from '~/types'; import { resolveHeaders, createSafeUser } from '~/utils/env'; @@ -162,6 +164,8 @@ export function getReasoningKey( type RunAgent = Omit & { tools?: GenericTool[]; maxContextTokens?: number; + /** Pre-ratio context budget from initializeAgent. */ + baseContextTokens?: number; useLegacyContent?: boolean; toolContextMap?: Record; toolRegistry?: LCToolRegistry; @@ -169,8 +173,65 @@ type RunAgent = Omit & { toolDefinitions?: LCTool[]; /** Precomputed flag indicating if any tools have defer_loading enabled */ hasDeferredTools?: boolean; + /** Optional per-agent summarization overrides */ + summarization?: SummarizationConfig; + /** + * Maximum characters allowed in a single tool result before truncation. + * Overrides the default computed from maxContextTokens. + */ + maxToolResultChars?: number; }; +function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + +/** Shapes a SummarizationConfig into the format expected by AgentInputs. */ +function shapeSummarizationConfig( + config: SummarizationConfig | undefined, + fallbackProvider: string, + fallbackModel: string | undefined, +) { + const provider = config?.provider ?? fallbackProvider; + const model = config?.model ?? fallbackModel; + const trigger = + config?.trigger?.type && config?.trigger?.value + ? { type: config.trigger.type, value: config.trigger.value } + : undefined; + + return { + enabled: config?.enabled !== false && isNonEmptyString(provider) && isNonEmptyString(model), + config: { + trigger, + provider, + model, + parameters: config?.parameters, + prompt: config?.prompt, + updatePrompt: config?.updatePrompt, + reserveRatio: config?.reserveRatio, + maxSummaryTokens: config?.maxSummaryTokens, + } satisfies AgentSummarizationConfig, + contextPruning: config?.contextPruning as ContextPruningConfig | undefined, + reserveRatio: config?.reserveRatio, + }; +} + +/** + * Applies `reserveRatio` against the pre-ratio base context budget, falling + * back to the pre-computed `maxContextTokens` from initializeAgent. + */ +function computeEffectiveMaxContextTokens( + reserveRatio: number | undefined, + baseContextTokens: number | undefined, + maxContextTokens: number | undefined, +): number | undefined { + if (reserveRatio == null || reserveRatio <= 0 || reserveRatio >= 1 || baseContextTokens == null) { + return maxContextTokens; + } + const ratioComputed = Math.max(1024, Math.round(baseContextTokens * (1 - reserveRatio))); + return Math.min(maxContextTokens ?? ratioComputed, ratioComputed); +} + /** * Creates a new Run instance with custom handlers and configuration. * @@ -196,6 +257,9 @@ export async function createRun({ tokenCounter, customHandlers, indexTokenCountMap, + summarizationConfig, + initialSummary, + calibrationRatio, streaming = true, streamUsage = true, }: { @@ -208,6 +272,11 @@ export async function createRun({ user?: IUser; /** Message history for extracting previously discovered tools */ messages?: BaseMessage[]; + summarizationConfig?: SummarizationConfig; + /** Cross-run summary from formatAgentMessages, forwarded to AgentContext */ + initialSummary?: { text: string; tokenCount: number }; + /** Calibration ratio from previous run's contextMeta, seeds the pruner EMA */ + calibrationRatio?: number; } & Pick): Promise< Run > { @@ -232,6 +301,13 @@ export async function createRun({ (providerEndpointMap[ agent.provider as keyof typeof providerEndpointMap ] as unknown as Providers) ?? agent.provider; + const selfModel = agent.model_parameters?.model ?? (agent.model as string | undefined); + + const summarization = shapeSummarizationConfig( + agent.summarization ?? summarizationConfig, + provider as string, + selfModel, + ); const llmConfig: t.RunLLMConfig = Object.assign( { @@ -299,6 +375,12 @@ export async function createRun({ } } + const effectiveMaxContextTokens = computeEffectiveMaxContextTokens( + summarization.reserveRatio, + agent.baseContextTokens, + agent.maxContextTokens, + ); + const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint); const agentInput: AgentInputs = { provider, @@ -310,9 +392,14 @@ export async function createRun({ instructions: systemContent, name: agent.name ?? undefined, toolRegistry: agent.toolRegistry, - maxContextTokens: agent.maxContextTokens, + maxContextTokens: effectiveMaxContextTokens, useLegacyContent: agent.useLegacyContent ?? false, discoveredTools: discoveredTools.size > 0 ? Array.from(discoveredTools) : undefined, + summarizationEnabled: summarization.enabled, + summarizationConfig: summarization.config, + initialSummary, + contextPruningConfig: summarization.contextPruning, + maxToolResultChars: agent.maxToolResultChars, }; agentInputs.push(agentInput); }; @@ -339,5 +426,6 @@ export async function createRun({ tokenCounter, customHandlers, indexTokenCountMap, + calibrationRatio, }); } diff --git a/packages/api/src/agents/usage.spec.ts b/packages/api/src/agents/usage.spec.ts index d0b065b8ff..b75baf69a8 100644 --- a/packages/api/src/agents/usage.spec.ts +++ b/packages/api/src/agents/usage.spec.ts @@ -108,6 +108,46 @@ describe('recordCollectedUsage', () => { }); }); + describe('summarization usage segregation', () => { + it('includes summarization output tokens in total while billing under separate context', async () => { + const collectedUsage: UsageMetadata[] = [ + { + usage_type: 'message', + input_tokens: 120, + output_tokens: 40, + model: 'gpt-4', + }, + { + usage_type: 'summarization', + input_tokens: 30, + output_tokens: 12, + model: 'gpt-4.1-mini', + }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(result).toEqual({ input_tokens: 120, output_tokens: 52 }); + expect(mockSpendTokens).toHaveBeenCalledTimes(2); + expect(mockSpendTokens).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ context: 'message', model: 'gpt-4' }), + expect.any(Object), + ); + expect(mockSpendTokens).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + context: 'summarization', + model: 'gpt-4.1-mini', + }), + expect.any(Object), + ); + }); + }); + describe('parallel execution (multiple agents)', () => { it('should handle parallel agents with independent input tokens', async () => { const collectedUsage: UsageMetadata[] = [ @@ -718,4 +758,130 @@ describe('recordCollectedUsage', () => { expect(result).toEqual({ input_tokens: 100, output_tokens: 50 }); }); }); + + describe('bulk write with summarization usage', () => { + let mockInsertMany: jest.Mock; + let mockUpdateBalance: jest.Mock; + let mockPricing: PricingFns; + let mockBulkWriteOps: BulkWriteDeps; + let bulkDeps: RecordUsageDeps; + + beforeEach(() => { + mockInsertMany = jest.fn().mockResolvedValue(undefined); + mockUpdateBalance = jest.fn().mockResolvedValue({}); + mockPricing = { + getMultiplier: jest.fn().mockReturnValue(1), + getCacheMultiplier: jest.fn().mockReturnValue(null), + }; + mockBulkWriteOps = { + insertMany: mockInsertMany, + updateBalance: mockUpdateBalance, + }; + bulkDeps = { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + pricing: mockPricing, + bulkWriteOps: mockBulkWriteOps, + }; + }); + + it('combines message and summarization docs into a single bulk write', async () => { + const collectedUsage: UsageMetadata[] = [ + { + usage_type: 'message', + input_tokens: 200, + output_tokens: 80, + model: 'gpt-4', + }, + { + usage_type: 'summarization', + input_tokens: 50, + output_tokens: 20, + model: 'gpt-4.1-mini', + }, + ]; + + const result = await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + + const insertedDocs = mockInsertMany.mock.calls[0][0]; + // 2 docs per entry (prompt + completion) x 2 entries = 4 docs + expect(insertedDocs).toHaveLength(4); + + const messageContextDocs = insertedDocs.filter( + (d: Record) => d.context === 'message', + ); + const summarizationContextDocs = insertedDocs.filter( + (d: Record) => d.context === 'summarization', + ); + expect(messageContextDocs).toHaveLength(2); + expect(summarizationContextDocs).toHaveLength(2); + + expect(result).toEqual({ input_tokens: 200, output_tokens: 100 }); + }); + + it('handles summarization-only usage in bulk mode', async () => { + const collectedUsage: UsageMetadata[] = [ + { + usage_type: 'summarization', + input_tokens: 60, + output_tokens: 25, + model: 'gpt-4.1-mini', + }, + ]; + + const result = await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + + const insertedDocs = mockInsertMany.mock.calls[0][0]; + expect(insertedDocs).toHaveLength(2); + + const summarizationContextDocs = insertedDocs.filter( + (d: Record) => d.context === 'summarization', + ); + expect(summarizationContextDocs).toHaveLength(2); + + expect(result).toEqual({ input_tokens: 0, output_tokens: 25 }); + }); + + it('handles message-only usage in bulk mode', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + + const insertedDocs = mockInsertMany.mock.calls[0][0]; + // 2 docs per entry x 2 entries = 4 docs + expect(insertedDocs).toHaveLength(4); + + const messageContextDocs = insertedDocs.filter( + (d: Record) => d.context === 'message', + ); + expect(messageContextDocs).toHaveLength(4); + + expect(result).toEqual({ input_tokens: 100, output_tokens: 110 }); + }); + }); }); diff --git a/packages/api/src/agents/usage.ts b/packages/api/src/agents/usage.ts index c092702730..3b2497c947 100644 --- a/packages/api/src/agents/usage.ts +++ b/packages/api/src/agents/usage.ts @@ -73,104 +73,125 @@ export async function recordCollectedUsage( return; } - const firstUsage = collectedUsage[0]; + const messageUsages: UsageMetadata[] = []; + const summarizationUsages: UsageMetadata[] = []; + for (const usage of collectedUsage) { + if (usage == null) { + continue; + } + (usage.usage_type === 'summarization' ? summarizationUsages : messageUsages).push(usage); + } + + const firstUsage = messageUsages[0]; const input_tokens = - (firstUsage?.input_tokens || 0) + - (Number(firstUsage?.input_token_details?.cache_creation) || - Number(firstUsage?.cache_creation_input_tokens) || - 0) + - (Number(firstUsage?.input_token_details?.cache_read) || - Number(firstUsage?.cache_read_input_tokens) || - 0); + firstUsage == null + ? 0 + : (firstUsage.input_tokens || 0) + + (Number(firstUsage.input_token_details?.cache_creation) || + Number(firstUsage.cache_creation_input_tokens) || + 0) + + (Number(firstUsage.input_token_details?.cache_read) || + Number(firstUsage.cache_read_input_tokens) || + 0); let total_output_tokens = 0; const { pricing, bulkWriteOps } = deps; const useBulk = pricing && bulkWriteOps; - const allDocs: PreparedEntry[] = []; + const processUsageGroup = ( + usages: UsageMetadata[], + usageContext: string, + docs: PreparedEntry[], + ): void => { + for (const usage of usages) { + if (!usage) { + continue; + } - for (const usage of collectedUsage) { - if (!usage) { - continue; - } + const cache_creation = + Number(usage.input_token_details?.cache_creation) || + Number(usage.cache_creation_input_tokens) || + 0; + const cache_read = + Number(usage.input_token_details?.cache_read) || Number(usage.cache_read_input_tokens) || 0; - const cache_creation = - Number(usage.input_token_details?.cache_creation) || - Number(usage.cache_creation_input_tokens) || - 0; - const cache_read = - Number(usage.input_token_details?.cache_read) || Number(usage.cache_read_input_tokens) || 0; + total_output_tokens += Number(usage.output_tokens) || 0; - total_output_tokens += Number(usage.output_tokens) || 0; + const txMetadata: TxMetadata = { + user, + balance, + messageId, + transactions, + conversationId, + endpointTokenConfig, + context: usageContext, + model: usage.model ?? model, + }; - const txMetadata: TxMetadata = { - user, - context, - balance, - messageId, - transactions, - conversationId, - endpointTokenConfig, - model: usage.model ?? model, - }; - - if (useBulk) { - const entries = - cache_creation > 0 || cache_read > 0 - ? prepareStructuredTokenSpend( - txMetadata, - { - promptTokens: { - input: usage.input_tokens, - write: cache_creation, - read: cache_read, + if (useBulk) { + const entries = + cache_creation > 0 || cache_read > 0 + ? prepareStructuredTokenSpend( + txMetadata, + { + promptTokens: { + input: usage.input_tokens, + write: cache_creation, + read: cache_read, + }, + completionTokens: usage.output_tokens, }, - completionTokens: usage.output_tokens, - }, - pricing, - ) - : prepareTokenSpend( - txMetadata, - { - promptTokens: usage.input_tokens, - completionTokens: usage.output_tokens, - }, - pricing, - ); - allDocs.push(...entries); - continue; - } + pricing, + ) + : prepareTokenSpend( + txMetadata, + { + promptTokens: usage.input_tokens, + completionTokens: usage.output_tokens, + }, + pricing, + ); + docs.push(...entries); + continue; + } + + if (cache_creation > 0 || cache_read > 0) { + deps + .spendStructuredTokens(txMetadata, { + promptTokens: { + input: usage.input_tokens, + write: cache_creation, + read: cache_read, + }, + completionTokens: usage.output_tokens, + }) + .catch((err) => { + logger.error( + `[packages/api #recordCollectedUsage] Error spending structured ${usageContext} tokens`, + err, + ); + }); + continue; + } - if (cache_creation > 0 || cache_read > 0) { deps - .spendStructuredTokens(txMetadata, { - promptTokens: { - input: usage.input_tokens, - write: cache_creation, - read: cache_read, - }, + .spendTokens(txMetadata, { + promptTokens: usage.input_tokens, completionTokens: usage.output_tokens, }) .catch((err) => { logger.error( - '[packages/api #recordCollectedUsage] Error spending structured tokens', + `[packages/api #recordCollectedUsage] Error spending ${usageContext} tokens`, err, ); }); - continue; } + }; - deps - .spendTokens(txMetadata, { - promptTokens: usage.input_tokens, - completionTokens: usage.output_tokens, - }) - .catch((err) => { - logger.error('[packages/api #recordCollectedUsage] Error spending tokens', err); - }); - } - + const allDocs: PreparedEntry[] = []; + processUsageGroup(messageUsages, context, allDocs); + processUsageGroup(summarizationUsages, 'summarization', allDocs); if (useBulk && allDocs.length > 0) { try { await bulkWriteTransactions({ user, docs: allDocs }, bulkWriteOps); diff --git a/packages/api/src/app/AppService.spec.ts b/packages/api/src/app/AppService.spec.ts index a7b5a46054..df607d612b 100644 --- a/packages/api/src/app/AppService.spec.ts +++ b/packages/api/src/app/AppService.spec.ts @@ -181,6 +181,43 @@ describe('AppService', () => { ); }); + it('should enable summarization when it is configured without enabled flag', async () => { + const config = { + summarization: { + prompt: 'Summarize with emphasis on next actions', + }, + } as Partial & { summarization: Record }; + + const result = await AppService({ config }); + expect(result).toEqual( + expect.objectContaining({ + summarization: expect.objectContaining({ + enabled: true, + prompt: 'Summarize with emphasis on next actions', + }), + }), + ); + }); + + it('should preserve explicit summarization disable flag', async () => { + const config = { + summarization: { + enabled: false, + prompt: 'Ignored while disabled', + }, + } as Partial & { summarization: Record }; + + const result = await AppService({ config }); + expect(result).toEqual( + expect.objectContaining({ + summarization: expect.objectContaining({ + enabled: false, + prompt: 'Ignored while disabled', + }), + }), + ); + }); + it('should load and format tools accurately with defined structure', async () => { const config = {}; diff --git a/packages/api/src/stream/interfaces/IJobStore.ts b/packages/api/src/stream/interfaces/IJobStore.ts index 5486b941eb..fadddb840d 100644 --- a/packages/api/src/stream/interfaces/IJobStore.ts +++ b/packages/api/src/stream/interfaces/IJobStore.ts @@ -65,12 +65,18 @@ export interface SerializableJobData { * ``` */ export interface UsageMetadata { + /** Logical usage bucket for accounting/reporting. Defaults to model response usage. */ + usage_type?: 'message' | 'summarization'; /** Total input tokens (prompt tokens) */ input_tokens?: number; /** Total output tokens (completion tokens) */ output_tokens?: number; + /** Total billed tokens when provided by the model/runtime */ + total_tokens?: number; /** Model identifier that generated this usage */ model?: string; + /** Provider identifier that generated this usage */ + provider?: string; /** * OpenAI-style cache token details. * Present for OpenAI models (GPT-4, o1, etc.) diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 50582832c0..2b4ac88245 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -24,6 +24,7 @@ export * from './text'; export * from './yaml'; export * from './http'; export * from './tokens'; +export * from './tokenMap'; export * from './url'; export * from './message'; export * from './tracing'; diff --git a/packages/api/src/utils/tokenMap.ts b/packages/api/src/utils/tokenMap.ts new file mode 100644 index 0000000000..71e2f65af6 --- /dev/null +++ b/packages/api/src/utils/tokenMap.ts @@ -0,0 +1,45 @@ +import type { BaseMessage } from '@langchain/core/messages'; + +/** Signature for a function that counts tokens in a LangChain message. */ +export type TokenCounter = (message: BaseMessage) => number; + +/** + * Lazily fills missing token counts for formatted LangChain messages. + * Preserves precomputed counts and only computes undefined indices. + * + * This is used after `formatAgentMessages` to ensure every message index + * has a token count before passing `indexTokenCountMap` to the agent run. + */ +export function hydrateMissingIndexTokenCounts({ + messages, + indexTokenCountMap, + tokenCounter, +}: { + messages: BaseMessage[]; + indexTokenCountMap: Record | undefined; + tokenCounter: TokenCounter; +}): Record { + const hydratedMap: Record = {}; + + if (indexTokenCountMap) { + for (const key in indexTokenCountMap) { + const tokenCount = indexTokenCountMap[Number(key)]; + if (typeof tokenCount === 'number' && Number.isFinite(tokenCount) && tokenCount > 0) { + hydratedMap[Number(key)] = tokenCount; + } + } + } + + for (let i = 0; i < messages.length; i++) { + if ( + typeof hydratedMap[i] === 'number' && + Number.isFinite(hydratedMap[i]) && + hydratedMap[i] > 0 + ) { + continue; + } + hydratedMap[i] = tokenCounter(messages[i]); + } + + return hydratedMap; +} diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 35411a1c9c..9bc3822c4b 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -205,6 +205,8 @@ export const baseEndpointSchema = z.object({ .optional(), titleEndpoint: z.string().optional(), titlePromptTemplate: z.string().optional(), + /** Maximum characters allowed in a single tool result before truncation. */ + maxToolResultChars: z.number().positive().optional(), }); export type TBaseEndpoint = z.infer; @@ -948,6 +950,34 @@ export const memorySchema = z.object({ export type TMemoryConfig = DeepPartial>; +export const summarizationTriggerSchema = z.object({ + type: z.enum(['token_count']), + value: z.number().positive(), +}); + +export const contextPruningSchema = z.object({ + enabled: z.boolean().optional(), + keepLastAssistants: z.number().min(0).max(10).optional(), + softTrimRatio: z.number().min(0).max(1).optional(), + hardClearRatio: z.number().min(0).max(1).optional(), + minPrunableToolChars: z.number().min(0).optional(), +}); + +export const summarizationConfigSchema = z.object({ + enabled: z.boolean().optional(), + provider: z.string().optional(), + model: z.string().optional(), + parameters: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(), + trigger: summarizationTriggerSchema.optional(), + prompt: z.string().optional(), + updatePrompt: z.string().optional(), + reserveRatio: z.number().min(0).max(1).optional(), + maxSummaryTokens: z.number().positive().optional(), + contextPruning: contextPruningSchema.optional(), +}); + +export type SummarizationConfig = z.infer; + const customEndpointsSchema = z.array(endpointSchema.partial()).optional(); export const configSchema = z.object({ @@ -956,6 +986,7 @@ export const configSchema = z.object({ ocr: ocrSchema.optional(), webSearch: webSearchSchema.optional(), memory: memorySchema.optional(), + summarization: summarizationConfigSchema.optional(), secureImageLinks: z.boolean().optional(), imageOutputType: z.nativeEnum(EImageOutputType).default(EImageOutputType.PNG), includedTools: z.array(z.string()).optional(), diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 7eb0482e9f..19ba804556 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -630,6 +630,18 @@ export const tMessageSchema = z.object({ feedback: feedbackSchema.optional(), /** metadata */ metadata: z.record(z.unknown()).optional(), + contextMeta: z + .object({ + calibrationRatio: z + .number() + .optional() + .describe('EMA ratio of provider-reported vs local token estimates; seeds the pruner on subsequent runs'), + encoding: z + .string() + .optional() + .describe('Tokenizer encoding used when this ratio was computed (e.g. "claude", "o200k_base")'), + }) + .optional(), }); export type MemoryArtifact = { diff --git a/packages/data-provider/src/types/agents.ts b/packages/data-provider/src/types/agents.ts index ac3f464019..db70de8c9d 100644 --- a/packages/data-provider/src/types/agents.ts +++ b/packages/data-provider/src/types/agents.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { StepTypes, ContentTypes, ToolCallTypes } from './runs'; +import type { FunctionToolCall, SummaryContentPart } from './assistants'; import type { TAttachment, TPlugin } from 'src/schemas'; -import type { FunctionToolCall } from './assistants'; export namespace Agents { export type MessageType = 'human' | 'ai' | 'generic' | 'system' | 'function' | 'tool' | 'remove'; @@ -53,6 +53,8 @@ export namespace Agents { | MessageContentImageUrl | MessageContentVideoUrl | MessageContentInputAudio + | SummaryContentPart + | ToolCallContent // eslint-disable-next-line @typescript-eslint/no-explicit-any | (Record & { type?: ContentTypes | string }) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -187,6 +189,7 @@ export namespace Agents { /** Group ID for parallel content - parts with same groupId are displayed in columns */ groupId?: number; // #new stepDetails: StepDetails; + summary?: SummaryContentPart; usage: null | object; }; @@ -313,6 +316,28 @@ export namespace Agents { | ContentTypes.VIDEO_URL | ContentTypes.INPUT_AUDIO | string; + + export interface SummarizeStartEvent { + agentId: string; + provider: string; + model?: string; + messagesToRefineCount: number; + summaryVersion: number; + } + + export interface SummarizeDeltaEvent { + id: string; + delta: { + summary: SummaryContentPart; + }; + } + + export interface SummarizeCompleteEvent { + id: string; + agentId: string; + summary?: SummaryContentPart; + error?: string; + } } export type ToolCallResult = { diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 22072403d3..690b2e06d2 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -521,6 +521,21 @@ export type ContentPart = ( export type TextData = (Text & PartMetadata) | undefined; +export type SummaryContentPart = { + type: ContentTypes.SUMMARY; + content?: Array<{ type: ContentTypes.TEXT; text: string }>; + tokenCount?: number; + summarizing?: boolean; + summaryVersion?: number; + model?: string; + provider?: string; + createdAt?: string; + boundary?: { + messageId: string; + contentIndex: number; + }; +}; + export type TMessageContentParts = | ({ type: ContentTypes.ERROR; @@ -545,6 +560,7 @@ export type TMessageContentParts = PartMetadata; } & ContentMetadata) | ({ type: ContentTypes.IMAGE_FILE; image_file: ImageFile & PartMetadata } & ContentMetadata) + | (SummaryContentPart & ContentMetadata) | (Agents.AgentUpdate & ContentMetadata) | (Agents.MessageContentImageUrl & ContentMetadata) | (Agents.MessageContentVideoUrl & ContentMetadata) diff --git a/packages/data-provider/src/types/runs.ts b/packages/data-provider/src/types/runs.ts index de61357b92..b159f99daf 100644 --- a/packages/data-provider/src/types/runs.ts +++ b/packages/data-provider/src/types/runs.ts @@ -8,6 +8,7 @@ export enum ContentTypes { VIDEO_URL = 'video_url', INPUT_AUDIO = 'input_audio', AGENT_UPDATE = 'agent_update', + SUMMARY = 'summary', ERROR = 'error', } @@ -24,3 +25,16 @@ export enum ToolCallTypes { /* Agents Tool Call */ TOOL_CALL = 'tool_call', } + +/** Event names dispatched by the agent graph and consumed by step handlers. */ +export enum StepEvents { + ON_RUN_STEP = 'on_run_step', + ON_AGENT_UPDATE = 'on_agent_update', + ON_MESSAGE_DELTA = 'on_message_delta', + ON_REASONING_DELTA = 'on_reasoning_delta', + ON_RUN_STEP_DELTA = 'on_run_step_delta', + ON_RUN_STEP_COMPLETED = 'on_run_step_completed', + ON_SUMMARIZE_START = 'on_summarize_start', + ON_SUMMARIZE_DELTA = 'on_summarize_delta', + ON_SUMMARIZE_COMPLETE = 'on_summarize_complete', +} diff --git a/packages/data-schemas/src/app/service.ts b/packages/data-schemas/src/app/service.ts index 9f9f521f59..91407b06c4 100644 --- a/packages/data-schemas/src/app/service.ts +++ b/packages/data-schemas/src/app/service.ts @@ -1,4 +1,8 @@ -import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider'; +import { + EModelEndpoint, + getConfigDefaults, + summarizationConfigSchema, +} from 'librechat-data-provider'; import type { TCustomConfig, FileSources, DeepPartial } from 'librechat-data-provider'; import type { AppConfig, FunctionTool } from '~/types/app'; import { loadDefaultInterface } from './interface'; @@ -9,6 +13,25 @@ import { processModelSpecs } from './specs'; import { loadMemoryConfig } from './memory'; import { loadEndpoints } from './endpoints'; import { loadOCRConfig } from './ocr'; +import logger from '~/config/winston'; + +function loadSummarizationConfig(config: DeepPartial): AppConfig['summarization'] { + const raw = config.summarization; + if (!raw || typeof raw !== 'object') { + return undefined; + } + + const parsed = summarizationConfigSchema.safeParse(raw); + if (!parsed.success) { + logger.warn('[AppService] Invalid summarization config', parsed.error.flatten()); + return undefined; + } + + return { + ...parsed.data, + enabled: parsed.data.enabled !== false, + }; +} export type Paths = { root: string; @@ -41,6 +64,7 @@ export const AppService = async (params?: { const ocr = loadOCRConfig(config.ocr); const webSearch = loadWebSearchConfig(config.webSearch); const memory = loadMemoryConfig(config.memory); + const summarization = loadSummarizationConfig(config); const filteredTools = config.filteredTools; const includedTools = config.includedTools; const fileStrategy = (config.fileStrategy ?? configDefaults.fileStrategy) as @@ -76,18 +100,19 @@ export const AppService = async (params?: { speech, balance, actions, - transactions, - mcpConfig: mcpServersConfig, - mcpSettings, webSearch, + mcpSettings, + transactions, fileStrategy, registration, filteredTools, includedTools, + summarization, availableTools, imageOutputType, interfaceConfig, turnstileConfig, + mcpConfig: mcpServersConfig, fileStrategies: config.fileStrategies, }; diff --git a/packages/data-schemas/src/schema/message.ts b/packages/data-schemas/src/schema/message.ts index 610251443d..ff3468918e 100644 --- a/packages/data-schemas/src/schema/message.ts +++ b/packages/data-schemas/src/schema/message.ts @@ -114,6 +114,14 @@ const messageSchema: Schema = new Schema( type: String, }, metadata: { type: mongoose.Schema.Types.Mixed }, + contextMeta: { + type: { + calibrationRatio: { type: Number }, + encoding: { type: String }, + }, + _id: false, + default: undefined, + }, attachments: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined }, /* attachments: { diff --git a/packages/data-schemas/src/types/app.ts b/packages/data-schemas/src/types/app.ts index 36891bfaec..73d65611b0 100644 --- a/packages/data-schemas/src/types/app.ts +++ b/packages/data-schemas/src/types/app.ts @@ -11,6 +11,7 @@ import type { TCustomEndpoints, TAssistantEndpoint, TAnthropicEndpoint, + SummarizationConfig, } from 'librechat-data-provider'; export type JsonSchemaType = { @@ -56,6 +57,8 @@ export interface AppConfig { }; /** Memory configuration */ memory?: TMemoryConfig; + /** Summarization configuration */ + summarization?: SummarizationConfig; /** Web search configuration */ webSearch?: TCustomConfig['webSearch']; /** File storage strategy ('local', 's3', 'firebase', 'azure_blob') */ diff --git a/packages/data-schemas/src/types/message.ts b/packages/data-schemas/src/types/message.ts index c3f465e711..201e5650ef 100644 --- a/packages/data-schemas/src/types/message.ts +++ b/packages/data-schemas/src/types/message.ts @@ -39,6 +39,10 @@ export interface IMessage extends Document { iconURL?: string; addedConvo?: boolean; metadata?: Record; + contextMeta?: { + calibrationRatio?: number; + encoding?: string; + }; attachments?: unknown[]; expiredAt?: Date | null; createdAt?: Date; From 7829fa9eca8b3b7c847421dc1dfa232359ac1e72 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 21 Mar 2026 13:01:59 -0400 Subject: [PATCH 103/111] =?UTF-8?q?=F0=9F=AA=84=20refactor:=20Simplify=20M?= =?UTF-8?q?CP=20Tool=20Content=20Formatting=20to=20Unified=20String=20Outp?= =?UTF-8?q?ut=20(#12352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Simplify content formatting in MCP service and parser - Consolidated content handling in `formatToolContent` to return a plain-text string instead of an array for all providers, enhancing clarity and consistency. - Removed unnecessary checks for content array providers, streamlining the logic for handling text and image artifacts. - Updated related tests to reflect changes in expected output format, ensuring comprehensive coverage for the new implementation. * fix: Return empty string for image-only tool responses instead of '(No response)' When artifacts exist (images/UI resources) but no text content is present, return an empty string rather than the misleading '(No response)' fallback. Adds missing test assertions for image-only content and standardizes length checks to explicit `> 0` comparisons. --- api/server/services/MCP.js | 11 +- .../api/src/mcp/__tests__/parsers.test.ts | 198 ++++++++---------- packages/api/src/mcp/parsers.ts | 38 +--- packages/api/src/mcp/types/index.ts | 2 +- 4 files changed, 95 insertions(+), 154 deletions(-) diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index c66eb0b6ef..5d97891c55 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -15,13 +15,7 @@ const { GenerationJobManager, resolveJsonSchemaRefs, } = require('@librechat/api'); -const { - Time, - CacheKeys, - Constants, - ContentTypes, - isAssistantsEndpoint, -} = require('librechat-data-provider'); +const { Time, CacheKeys, Constants, isAssistantsEndpoint } = require('librechat-data-provider'); const { getOAuthReconnectionManager, getMCPServersRegistry, @@ -605,9 +599,6 @@ function createToolInstance({ if (isAssistantsEndpoint(provider) && Array.isArray(result)) { return result[0]; } - if (isGoogle && Array.isArray(result[0]) && result[0][0]?.type === ContentTypes.TEXT) { - return [result[0][0].text, result[1]]; - } return result; } catch (error) { logger.error( diff --git a/packages/api/src/mcp/__tests__/parsers.test.ts b/packages/api/src/mcp/__tests__/parsers.test.ts index dd9a09a0fb..afc3fd9de3 100644 --- a/packages/api/src/mcp/__tests__/parsers.test.ts +++ b/packages/api/src/mcp/__tests__/parsers.test.ts @@ -31,12 +31,22 @@ describe('formatToolContent', () => { }); }); - describe('recognized providers - content array providers', () => { - const contentArrayProviders: t.Provider[] = ['google', 'anthropic', 'openai', 'azureopenai']; + describe('recognized providers', () => { + const allProviders: t.Provider[] = [ + 'google', + 'anthropic', + 'openai', + 'azureopenai', + 'openrouter', + 'xai', + 'deepseek', + 'ollama', + 'bedrock', + ]; - contentArrayProviders.forEach((provider) => { + allProviders.forEach((provider) => { describe(`${provider} provider`, () => { - it('should format text content as content array', () => { + it('should format text content as string', () => { const result: t.MCPToolCallResponse = { content: [ { type: 'text', text: 'First text' }, @@ -45,11 +55,11 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, provider); - expect(content).toEqual([{ type: 'text', text: 'First text\n\nSecond text' }]); + expect(content).toBe('First text\n\nSecond text'); expect(artifacts).toBeUndefined(); }); - it('should separate text blocks when images are present', () => { + it('should extract images to artifacts and keep text as string', () => { const result: t.MCPToolCallResponse = { content: [ { type: 'text', text: 'Before image' }, @@ -59,10 +69,7 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, provider); - expect(content).toEqual([ - { type: 'text', text: 'Before image' }, - { type: 'text', text: 'After image' }, - ]); + expect(content).toBe('Before image\n\nAfter image'); expect(artifacts).toEqual({ content: [ { @@ -76,62 +83,21 @@ describe('formatToolContent', () => { it('should handle empty content', () => { const result: t.MCPToolCallResponse = { content: [] }; const [content, artifacts] = formatToolContent(result, provider); - expect(content).toEqual([{ type: 'text', text: '(No response)' }]); + expect(content).toBe('(No response)'); expect(artifacts).toBeUndefined(); }); }); }); }); - describe('recognized providers - string providers', () => { - const stringProviders: t.Provider[] = ['openrouter', 'xai', 'deepseek', 'ollama', 'bedrock']; - - stringProviders.forEach((provider) => { - describe(`${provider} provider`, () => { - it('should format content as string', () => { - const result: t.MCPToolCallResponse = { - content: [ - { type: 'text', text: 'First text' }, - { type: 'text', text: 'Second text' }, - ], - }; - - const [content, artifacts] = formatToolContent(result, provider); - expect(content).toBe('First text\n\nSecond text'); - expect(artifacts).toBeUndefined(); - }); - - it('should handle images with string output', () => { - const result: t.MCPToolCallResponse = { - content: [ - { type: 'text', text: 'Some text' }, - { type: 'image', data: 'base64data', mimeType: 'image/png' }, - ], - }; - - const [content, artifacts] = formatToolContent(result, provider); - expect(content).toBe('Some text'); - expect(artifacts).toEqual({ - content: [ - { - type: 'image_url', - image_url: { url: 'data:image/png;base64,base64data' }, - }, - ], - }); - }); - }); - }); - }); - describe('image handling', () => { it('should handle images with http URLs', () => { const result: t.MCPToolCallResponse = { content: [{ type: 'image', data: 'https://example.com/image.png', mimeType: 'image/png' }], }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_content, artifacts] = formatToolContent(result, 'openai'); + const [content, artifacts] = formatToolContent(result, 'openai'); + expect(content).toBe(''); expect(artifacts).toEqual({ content: [ { @@ -147,8 +113,8 @@ describe('formatToolContent', () => { content: [{ type: 'image', data: 'iVBORw0KGgoAAAA...', mimeType: 'image/png' }], }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_content, artifacts] = formatToolContent(result, 'openai'); + const [content, artifacts] = formatToolContent(result, 'openai'); + expect(content).toBe(''); expect(artifacts).toEqual({ content: [ { @@ -158,6 +124,29 @@ describe('formatToolContent', () => { ], }); }); + + it('should return empty string for image-only content when artifacts exist', () => { + const result: t.MCPToolCallResponse = { + content: [{ type: 'image', data: 'base64data', mimeType: 'image/png' }], + }; + const [content, artifacts] = formatToolContent(result, 'anthropic'); + expect(content).toBe(''); + expect(artifacts).toBeDefined(); + expect(artifacts?.content).toHaveLength(1); + }); + + it('should handle multiple images without text', () => { + const result: t.MCPToolCallResponse = { + content: [ + { type: 'image', data: 'https://example.com/a.png', mimeType: 'image/png' }, + { type: 'image', data: 'https://example.com/b.jpg', mimeType: 'image/jpeg' }, + ], + }; + const [content, artifacts] = formatToolContent(result, 'google'); + expect(content).toBe(''); + expect(artifacts).toBeDefined(); + expect(artifacts?.content).toHaveLength(2); + }); }); describe('resource handling', () => { @@ -176,13 +165,11 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'openai'); - expect(Array.isArray(content)).toBe(true); - const textContent = Array.isArray(content) ? content[0] : { text: '' }; - expect(textContent).toMatchObject({ type: 'text' }); - expect(textContent.text).toContain('UI Resource ID:'); - expect(textContent.text).toContain('UI Resource Marker: \\ui{'); - expect(textContent.text).toContain('Resource URI: ui://carousel'); - expect(textContent.text).toContain('Resource MIME Type: application/json'); + expect(typeof content).toBe('string'); + expect(content).toContain('UI Resource ID:'); + expect(content).toContain('UI Resource Marker: \\ui{'); + expect(content).toContain('Resource URI: ui://carousel'); + expect(content).toContain('Resource MIME Type: application/json'); const uiResourceArtifact = artifacts?.ui_resources?.data?.[0]; expect(uiResourceArtifact).toBeTruthy(); @@ -209,15 +196,11 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'openai'); - expect(content).toEqual([ - { - type: 'text', - text: - 'Resource Text: Document content\n' + - 'Resource URI: file://document.pdf\n' + - 'Resource MIME Type: application/pdf', - }, - ]); + expect(content).toBe( + 'Resource Text: Document content\n' + + 'Resource URI: file://document.pdf\n' + + 'Resource MIME Type: application/pdf', + ); expect(artifacts).toBeUndefined(); }); @@ -235,12 +218,7 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'openai'); - expect(content).toEqual([ - { - type: 'text', - text: 'Resource URI: https://example.com/resource', - }, - ]); + expect(content).toBe('Resource URI: https://example.com/resource'); expect(artifacts).toBeUndefined(); }); @@ -267,14 +245,12 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'openai'); - expect(Array.isArray(content)).toBe(true); - const textEntry = Array.isArray(content) ? content[0] : { text: '' }; - expect(textEntry).toMatchObject({ type: 'text' }); - expect(textEntry.text).toContain('Some text'); - expect(textEntry.text).toContain('UI Resource Marker: \\ui{'); - expect(textEntry.text).toContain('Resource URI: ui://button'); - expect(textEntry.text).toContain('Resource MIME Type: application/json'); - expect(textEntry.text).toContain('Resource URI: file://data.csv'); + expect(typeof content).toBe('string'); + expect(content).toContain('Some text'); + expect(content).toContain('UI Resource Marker: \\ui{'); + expect(content).toContain('Resource URI: ui://button'); + expect(content).toContain('Resource MIME Type: application/json'); + expect(content).toContain('Resource URI: file://data.csv'); const uiResource = artifacts?.ui_resources?.data?.[0]; expect(uiResource).toMatchObject({ @@ -302,14 +278,11 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'openai'); - expect(Array.isArray(content)).toBe(true); - if (Array.isArray(content)) { - expect(content[0]).toMatchObject({ type: 'text', text: 'Content with multimedia' }); - expect(content[1].type).toBe('text'); - expect(content[1].text).toContain('UI Resource Marker: \\ui{'); - expect(content[1].text).toContain('Resource URI: ui://graph'); - expect(content[1].text).toContain('Resource MIME Type: application/json'); - } + expect(typeof content).toBe('string'); + expect(content).toContain('Content with multimedia'); + expect(content).toContain('UI Resource Marker: \\ui{'); + expect(content).toContain('Resource URI: ui://graph'); + expect(content).toContain('Resource MIME Type: application/json'); expect(artifacts).toEqual({ content: [ { @@ -341,12 +314,9 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'openai'); - expect(content).toEqual([ - { - type: 'text', - text: 'Normal text\n\n' + JSON.stringify({ type: 'unknown', data: 'some data' }, null, 2), - }, - ]); + expect(content).toBe( + 'Normal text\n\n' + JSON.stringify({ type: 'unknown', data: 'some data' }, null, 2), + ); expect(artifacts).toBeUndefined(); }); }); @@ -379,20 +349,16 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'anthropic'); - expect(Array.isArray(content)).toBe(true); - if (Array.isArray(content)) { - expect(content[0]).toEqual({ type: 'text', text: 'Introduction' }); - expect(content[1].type).toBe('text'); - expect(content[1].text).toContain('Middle section'); - expect(content[1].text).toContain('UI Resource ID:'); - expect(content[1].text).toContain('UI Resource Marker: \\ui{'); - expect(content[1].text).toContain('Resource URI: ui://chart'); - expect(content[1].text).toContain('Resource MIME Type: application/json'); - expect(content[1].text).toContain('Resource URI: https://api.example.com/data'); - expect(content[2].type).toBe('text'); - expect(content[2].text).toContain('Conclusion'); - expect(content[2].text).toContain('UI Resource Markers Available:'); - } + expect(typeof content).toBe('string'); + expect(content).toContain('Introduction'); + expect(content).toContain('Middle section'); + expect(content).toContain('UI Resource ID:'); + expect(content).toContain('UI Resource Marker: \\ui{'); + expect(content).toContain('Resource URI: ui://chart'); + expect(content).toContain('Resource MIME Type: application/json'); + expect(content).toContain('Resource URI: https://api.example.com/data'); + expect(content).toContain('Conclusion'); + expect(content).toContain('UI Resource Markers Available:'); expect(artifacts).toMatchObject({ content: [ { @@ -424,7 +390,7 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'openai'); - expect(content).toEqual([{ type: 'text', text: 'Error occurred' }]); + expect(content).toBe('Error occurred'); expect(artifacts).toBeUndefined(); }); @@ -435,7 +401,7 @@ describe('formatToolContent', () => { }; const [content, artifacts] = formatToolContent(result, 'google'); - expect(content).toEqual([{ type: 'text', text: 'Response with metadata' }]); + expect(content).toBe('Response with metadata'); expect(artifacts).toBeUndefined(); }); }); diff --git a/packages/api/src/mcp/parsers.ts b/packages/api/src/mcp/parsers.ts index 76e59b2e9c..c9b824e782 100644 --- a/packages/api/src/mcp/parsers.ts +++ b/packages/api/src/mcp/parsers.ts @@ -18,7 +18,6 @@ const RECOGNIZED_PROVIDERS = new Set([ 'ollama', 'bedrock', ]); -const CONTENT_ARRAY_PROVIDERS = new Set(['google', 'anthropic', 'azureopenai', 'openai']); const imageFormatters: Record = { // google: (item) => ({ @@ -81,13 +80,13 @@ function parseAsString(result: t.MCPToolCallResponse): string { } /** - * Converts MCPToolCallResponse content into recognized content block types - * First element: string or formatted content (excluding image_url) - * Second element: Recognized types - "image", "image_url", "text", "json" + * Converts MCPToolCallResponse content into a plain-text string plus optional artifacts + * (images, UI resources). All providers receive string content; images are separated into + * artifacts and merged back by the agents package via formatArtifactPayload / formatAnthropicArtifactContent. * - * @param result - The MCPToolCallResponse object - * @param provider - The provider name (google, anthropic, openai) - * @returns Tuple of content and image_urls + * @param provider - Used only to distinguish recognized vs. unrecognized providers. + * All recognized providers currently produce identical string output; + * provider-specific artifact merging is delegated to the agents package. */ export function formatToolContent( result: t.MCPToolCallResponse, @@ -99,13 +98,12 @@ export function formatToolContent( const content = result?.content ?? []; if (!content.length) { - return [[{ type: 'text', text: '(No response)' }], undefined]; + return ['(No response)', undefined]; } - const formattedContent: t.FormattedContent[] = []; const imageUrls: t.FormattedContent[] = []; - let currentTextBlock = ''; const uiResources: UIResource[] = []; + let currentTextBlock = ''; type ContentHandler = undefined | ((item: t.ToolContentPart) => void); @@ -122,17 +120,11 @@ export function formatToolContent( if (!isImageContent(item)) { return; } - if (CONTENT_ARRAY_PROVIDERS.has(provider) && currentTextBlock) { - formattedContent.push({ type: 'text', text: currentTextBlock }); - currentTextBlock = ''; - } const formatter = imageFormatters.default as t.ImageFormatter; const formattedImage = formatter(item); if (formattedImage.type === 'image_url') { imageUrls.push(formattedImage); - } else { - formattedContent.push(formattedImage); } }, @@ -195,25 +187,17 @@ UI Resource Markers Available: currentTextBlock += uiInstructions; } - if (CONTENT_ARRAY_PROVIDERS.has(provider) && currentTextBlock) { - formattedContent.push({ type: 'text', text: currentTextBlock }); - } - let artifacts: t.Artifacts = undefined; - if (imageUrls.length) { + if (imageUrls.length > 0) { artifacts = { content: imageUrls }; } - if (uiResources.length) { + if (uiResources.length > 0) { artifacts = { ...artifacts, [Tools.ui_resources]: { data: uiResources }, }; } - if (CONTENT_ARRAY_PROVIDERS.has(provider)) { - return [formattedContent, artifacts]; - } - - return [currentTextBlock, artifacts]; + return [currentTextBlock || (artifacts !== undefined ? '' : '(No response)'), artifacts]; } diff --git a/packages/api/src/mcp/types/index.ts b/packages/api/src/mcp/types/index.ts index 9d43aa543d..6cb5e02f0b 100644 --- a/packages/api/src/mcp/types/index.ts +++ b/packages/api/src/mcp/types/index.ts @@ -138,7 +138,7 @@ export type Artifacts = } | undefined; -export type FormattedContentResult = [string | FormattedContent[], undefined | Artifacts]; +export type FormattedContentResult = [string, Artifacts | undefined]; export type ImageFormatter = (item: ImageContent) => FormattedContent; From 87a3b8221afb34b448f8d73954cfea5702beceaf Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 21 Mar 2026 13:59:00 -0400 Subject: [PATCH 104/111] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Consolidate=20g?= =?UTF-8?q?etSoleOwnedResourceIds=20into=20data-schemas=20and=20use=20db?= =?UTF-8?q?=20object=20in=20PermissionService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move getSoleOwnedResourceIds from PermissionService to data-schemas aclEntry methods, update PermissionService to use the db object pattern instead of destructured imports from ~/models, and update UserController + tests accordingly. --- api/server/controllers/UserController.js | 3 +- .../controllers/__tests__/deleteUser.spec.js | 5 +- api/server/services/PermissionService.js | 128 ++++-------------- 3 files changed, 31 insertions(+), 105 deletions(-) diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 3702f190db..301c6d2f76 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -22,7 +22,6 @@ const { getMCPManager, getFlowStateManager, getMCPServersRegistry } = require('~ const { invalidateCachedTools } = require('~/server/services/Config/getCachedTools'); const { processDeleteRequest } = require('~/server/services/Files/process'); const { getAppConfig } = require('~/server/services/Config'); -const { getSoleOwnedResourceIds } = require('~/server/services/PermissionService'); const { getLogStores } = require('~/cache'); const db = require('~/models'); @@ -111,7 +110,7 @@ const deleteUserMcpServers = async (userId) => { } const userObjectId = new mongoose.Types.ObjectId(userId); - const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.MCPSERVER); + const soleOwnedIds = await db.getSoleOwnedResourceIds(userObjectId, ResourceType.MCPSERVER); const authoredServers = await MCPServer.find({ author: userObjectId }) .select('_id serverName') diff --git a/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js index 8dcd217657..a382a6cdc7 100644 --- a/api/server/controllers/__tests__/deleteUser.spec.js +++ b/api/server/controllers/__tests__/deleteUser.spec.js @@ -66,6 +66,7 @@ jest.mock('~/models', () => ({ deleteTokens: jest.fn(), removeUserFromAllGroups: jest.fn(), deleteAclEntries: jest.fn(), + getSoleOwnedResourceIds: jest.fn().mockResolvedValue([]), })); jest.mock('~/server/services/PluginService', () => ({ @@ -100,10 +101,6 @@ jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn(), })); -jest.mock('~/server/services/PermissionService', () => ({ - getSoleOwnedResourceIds: jest.fn().mockResolvedValue([]), -})); - jest.mock('~/cache', () => ({ getLogStores: jest.fn(), })); diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index f7b6be612f..fc67b0bc49 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -1,12 +1,7 @@ const mongoose = require('mongoose'); const { isEnabled } = require('@librechat/api'); const { getTransactionSupport, logger } = require('@librechat/data-schemas'); -const { - ResourceType, - PrincipalType, - PrincipalModel, - PermissionBits, -} = require('librechat-data-provider'); +const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider'); const { entraIdPrincipalFeatureEnabled, getUserOwnedEntraGroups, @@ -14,28 +9,7 @@ const { getGroupMembers, getGroupOwners, } = require('~/server/services/GraphApiService'); -const { - findAccessibleResources: findAccessibleResourcesACL, - getEffectivePermissions: getEffectivePermissionsACL, - getEffectivePermissionsForResources: getEffectivePermissionsForResourcesACL, - grantPermission: grantPermissionACL, - findEntriesByPrincipalsAndResource, - findRolesByResourceType, - findPublicResourceIds, - bulkWriteAclEntries, - findGroupByExternalId, - findRoleByIdentifier, - deleteAclEntries, - getUserPrincipals, - findGroupByQuery, - updateGroupById, - bulkUpdateGroups, - hasPermission, - createGroup, - createUser, - updateUser, - findUser, -} = require('~/models'); +const db = require('~/models'); /** @type {boolean|null} */ let transactionSupportCache = null; @@ -107,7 +81,7 @@ const grantPermission = async ({ validateResourceType(resourceType); // Get the role to determine permission bits - const role = await findRoleByIdentifier(accessRoleId); + const role = await db.findRoleByIdentifier(accessRoleId); if (!role) { throw new Error(`Role ${accessRoleId} not found`); } @@ -118,7 +92,7 @@ const grantPermission = async ({ `Role ${accessRoleId} is for ${role.resourceType} resources, not ${resourceType}`, ); } - return await grantPermissionACL( + return await db.grantPermission( principalType, principalId, resourceType, @@ -152,13 +126,13 @@ const checkPermission = async ({ userId, role, resourceType, resourceId, require validateResourceType(resourceType); - const principals = await getUserPrincipals({ userId, role }); + const principals = await db.getUserPrincipals({ userId, role }); if (principals.length === 0) { return false; } - return await hasPermission(principals, resourceType, resourceId, requiredPermission); + return await db.hasPermission(principals, resourceType, resourceId, requiredPermission); } catch (error) { logger.error(`[PermissionService.checkPermission] Error: ${error.message}`); if (error.message.includes('requiredPermission must be')) { @@ -181,13 +155,13 @@ const getEffectivePermissions = async ({ userId, role, resourceType, resourceId try { validateResourceType(resourceType); - const principals = await getUserPrincipals({ userId, role }); + const principals = await db.getUserPrincipals({ userId, role }); if (principals.length === 0) { return 0; } - return await getEffectivePermissionsACL(principals, resourceType, resourceId); + return await db.getEffectivePermissions(principals, resourceType, resourceId); } catch (error) { logger.error(`[PermissionService.getEffectivePermissions] Error: ${error.message}`); return 0; @@ -217,10 +191,10 @@ const getResourcePermissionsMap = async ({ userId, role, resourceType, resourceI try { // Get user principals (user + groups + public) - const principals = await getUserPrincipals({ userId, role }); + const principals = await db.getUserPrincipals({ userId, role }); // Use batch method from aclEntry - const permissionsMap = await getEffectivePermissionsForResourcesACL( + const permissionsMap = await db.getEffectivePermissionsForResources( principals, resourceType, resourceIds, @@ -255,12 +229,12 @@ const findAccessibleResources = async ({ userId, role, resourceType, requiredPer validateResourceType(resourceType); // Get all principals for the user (user + groups + public) - const principalsList = await getUserPrincipals({ userId, role }); + const principalsList = await db.getUserPrincipals({ userId, role }); if (principalsList.length === 0) { return []; } - return await findAccessibleResourcesACL(principalsList, resourceType, requiredPermissions); + return await db.findAccessibleResources(principalsList, resourceType, requiredPermissions); } catch (error) { logger.error(`[PermissionService.findAccessibleResources] Error: ${error.message}`); // Re-throw validation errors @@ -286,7 +260,7 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio validateResourceType(resourceType); - return await findPublicResourceIds(resourceType, requiredPermissions); + return await db.findPublicResourceIds(resourceType, requiredPermissions); } catch (error) { logger.error(`[PermissionService.findPubliclyAccessibleResources] Error: ${error.message}`); if (error.message.includes('requiredPermissions must be')) { @@ -305,7 +279,7 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio const getAvailableRoles = async ({ resourceType }) => { validateResourceType(resourceType); - return await findRolesByResourceType(resourceType); + return await db.findRolesByResourceType(resourceType); }; /** @@ -334,15 +308,15 @@ const ensurePrincipalExists = async function (principal) { throw new Error('Entra ID user principals must have email and idOnTheSource'); } - let existingUser = await findUser({ idOnTheSource: principal.idOnTheSource }); + let existingUser = await db.findUser({ idOnTheSource: principal.idOnTheSource }); if (!existingUser) { - existingUser = await findUser({ email: principal.email }); + existingUser = await db.findUser({ email: principal.email }); } if (existingUser) { if (!existingUser.idOnTheSource && principal.idOnTheSource) { - await updateUser(existingUser._id, { + await db.updateUser(existingUser._id, { idOnTheSource: principal.idOnTheSource, provider: 'openid', }); @@ -358,7 +332,7 @@ const ensurePrincipalExists = async function (principal) { idOnTheSource: principal.idOnTheSource, }; - const userId = await createUser(userData, true, true); + const userId = await db.createUser(userData, true, true); return userId.toString(); } @@ -423,10 +397,10 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null } } - let existingGroup = await findGroupByExternalId(principal.idOnTheSource, 'entra'); + let existingGroup = await db.findGroupByExternalId(principal.idOnTheSource, 'entra'); if (!existingGroup && principal.email) { - existingGroup = await findGroupByQuery({ email: principal.email.toLowerCase() }); + existingGroup = await db.findGroupByQuery({ email: principal.email.toLowerCase() }); } if (existingGroup) { @@ -455,7 +429,7 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null } if (needsUpdate) { - await updateGroupById(existingGroup._id, updateData); + await db.updateGroupById(existingGroup._id, updateData); } return existingGroup._id.toString(); @@ -476,7 +450,7 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null groupData.description = principal.description; } - const newGroup = await createGroup(groupData); + const newGroup = await db.createGroup(groupData); return newGroup._id.toString(); } if (principal.id && authContext == null) { @@ -523,7 +497,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null) const sessionOptions = session ? { session } : {}; - await bulkUpdateGroups( + await db.bulkUpdateGroups( { idOnTheSource: { $in: allGroupIds }, source: 'entra', @@ -533,7 +507,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null) sessionOptions, ); - await bulkUpdateGroups( + await db.bulkUpdateGroups( { source: 'entra', memberIds: user.idOnTheSource, @@ -566,7 +540,7 @@ const hasPublicPermission = async ({ resourceType, resourceId, requiredPermissio // Use public principal to check permissions const publicPrincipal = [{ principalType: PrincipalType.PUBLIC }]; - const entries = await findEntriesByPrincipalsAndResource( + const entries = await db.findEntriesByPrincipalsAndResource( publicPrincipal, resourceType, resourceId, @@ -631,7 +605,7 @@ const bulkUpdateResourcePermissions = async ({ const sessionOptions = localSession ? { session: localSession } : {}; - const roles = await findRolesByResourceType(resourceType); + const roles = await db.findRolesByResourceType(resourceType); const rolesMap = new Map(); roles.forEach((role) => { rolesMap.set(role.accessRoleId, role); @@ -735,7 +709,7 @@ const bulkUpdateResourcePermissions = async ({ } if (bulkWrites.length > 0) { - await bulkWriteAclEntries(bulkWrites, sessionOptions); + await db.bulkWriteAclEntries(bulkWrites, sessionOptions); } const deleteQueries = []; @@ -776,7 +750,7 @@ const bulkUpdateResourcePermissions = async ({ } if (deleteQueries.length > 0) { - await deleteAclEntries({ $or: deleteQueries }, sessionOptions); + await db.deleteAclEntries({ $or: deleteQueries }, sessionOptions); } if (shouldEndSession && supportsTransactions) { @@ -805,49 +779,6 @@ const bulkUpdateResourcePermissions = async ({ } }; -/** - * Returns resource IDs where the given user is the sole owner - * (no other principal holds the DELETE bit on the same resource). - * @param {mongoose.Types.ObjectId} userObjectId - * @param {string|string[]} resourceTypes - One or more ResourceType values. - * @returns {Promise} - */ -const getSoleOwnedResourceIds = async (userObjectId, resourceTypes) => { - const types = Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes]; - const ownedEntries = await AclEntry.find({ - principalType: PrincipalType.USER, - principalId: userObjectId, - resourceType: { $in: types }, - permBits: { $bitsAllSet: PermissionBits.DELETE }, - }) - .select('resourceId') - .lean(); - - if (ownedEntries.length === 0) { - return []; - } - - const ownedIds = ownedEntries.map((e) => e.resourceId); - - const otherOwners = await AclEntry.aggregate([ - { - $match: { - resourceType: { $in: types }, - resourceId: { $in: ownedIds }, - permBits: { $bitsAllSet: PermissionBits.DELETE }, - $or: [ - { principalId: { $ne: userObjectId } }, - { principalType: { $ne: PrincipalType.USER } }, - ], - }, - }, - { $group: { _id: '$resourceId' } }, - ]); - - const multiOwnerIds = new Set(otherOwners.map((doc) => doc._id.toString())); - return ownedIds.filter((id) => !multiOwnerIds.has(id.toString())); -}; - /** * Remove all permissions for a resource (cleanup when resource is deleted) * @param {Object} params - Parameters for removing all permissions @@ -863,7 +794,7 @@ const removeAllPermissions = async ({ resourceType, resourceId }) => { throw new Error(`Invalid resource ID: ${resourceId}`); } - const result = await deleteAclEntries({ + const result = await db.deleteAclEntries({ resourceType, resourceId, }); @@ -888,6 +819,5 @@ module.exports = { ensurePrincipalExists, ensureGroupPrincipalExists, syncUserEntraGroupMemberships, - getSoleOwnedResourceIds, removeAllPermissions, }; From 150361273f823a9d58b6ddc23f20ac30f5202442 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 21 Mar 2026 14:30:46 -0400 Subject: [PATCH 105/111] =?UTF-8?q?=F0=9F=A7=BD=20chore:=20Resolve=20TypeS?= =?UTF-8?q?cript=20errors=20and=20test=20failures=20in=20agent/prompt=20de?= =?UTF-8?q?letion=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type AclEntry model as Model instead of Model in deleteUserAgents and deleteUserPrompts, wire getSoleOwnedResourceIds into agent.spec.ts via createAclEntryMethods, replace permissionService calls with direct AclEntry.create, and add missing principalModel field. --- .../data-schemas/src/methods/agent.spec.ts | 69 ++++++++++++++----- packages/data-schemas/src/methods/agent.ts | 10 ++- packages/data-schemas/src/methods/prompt.ts | 14 ++-- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/packages/data-schemas/src/methods/agent.spec.ts b/packages/data-schemas/src/methods/agent.spec.ts index f828c8c325..3184f51fa1 100644 --- a/packages/data-schemas/src/methods/agent.spec.ts +++ b/packages/data-schemas/src/methods/agent.spec.ts @@ -17,6 +17,7 @@ import type { } from 'mongoose'; import type { IAgent, IAclEntry, IUser, IAccessRole } from '..'; import { createAgentMethods, type AgentMethods } from './agent'; +import { createAclEntryMethods } from './aclEntry'; import { createModels } from '~/models'; /** Version snapshot stored in `IAgent.versions[]`. Extends the base omit with runtime-only fields. */ @@ -76,7 +77,14 @@ beforeAll(async () => { await AclEntry.deleteMany({ resourceType, resourceId }); }; - methods = createAgentMethods(mongoose, { removeAllPermissions, getActions }); + const aclEntryMethods = createAclEntryMethods(mongoose); + const { getSoleOwnedResourceIds } = aclEntryMethods; + + methods = createAgentMethods(mongoose, { + removeAllPermissions, + getActions, + getSoleOwnedResourceIds, + }); createAgent = methods.createAgent; getAgent = methods.getAgent; updateAgent = methods.updateAgent; @@ -932,21 +940,27 @@ describe('Agent Methods', () => { author: otherAuthorId, }); - await permissionService.grantPermission({ + const ownerBits = + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE; + await AclEntry.create({ principalType: PrincipalType.USER, principalId: authorId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: agent1._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, + permBits: ownerBits, grantedBy: authorId, + grantedAt: new Date(), }); - await permissionService.grantPermission({ + await AclEntry.create({ principalType: PrincipalType.USER, principalId: authorId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: agent2._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, + permBits: ownerBits, grantedBy: authorId, + grantedAt: new Date(), }); await User.create({ @@ -1016,21 +1030,27 @@ describe('Agent Methods', () => { author: authorId, }); - await permissionService.grantPermission({ + const ownerBits = + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE; + await AclEntry.create({ principalType: PrincipalType.USER, principalId: authorId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: agent1._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, + permBits: ownerBits, grantedBy: authorId, + grantedAt: new Date(), }); - await permissionService.grantPermission({ + await AclEntry.create({ principalType: PrincipalType.USER, principalId: authorId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: agent2._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, + permBits: ownerBits, grantedBy: authorId, + grantedAt: new Date(), }); await User.create({ @@ -1096,13 +1116,17 @@ describe('Agent Methods', () => { author: otherAuthorId, }); - await permissionService.grantPermission({ + const ownerBits = + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE; + await AclEntry.create({ principalType: PrincipalType.USER, principalId: otherAuthorId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: existingAgent._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, + permBits: ownerBits, grantedBy: otherAuthorId, + grantedAt: new Date(), }); await User.create({ @@ -1150,21 +1174,27 @@ describe('Agent Methods', () => { author: authorId, }); - await permissionService.grantPermission({ + const ownerBits = + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE; + await AclEntry.create({ principalType: PrincipalType.USER, principalId: authorId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: agent1._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, + permBits: ownerBits, grantedBy: authorId, + grantedAt: new Date(), }); - await permissionService.grantPermission({ + await AclEntry.create({ principalType: PrincipalType.USER, principalId: authorId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: agent2._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, + permBits: ownerBits, grantedBy: authorId, + grantedAt: new Date(), }); await User.create({ @@ -1216,24 +1246,27 @@ describe('Agent Methods', () => { await AclEntry.create({ principalType: PrincipalType.USER, principalId: deletingUserId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: (soleAgent as unknown as { _id: mongoose.Types.ObjectId })._id, - permBits: PermissionBits.DELETE | PermissionBits.READ | PermissionBits.WRITE, + permBits: PermissionBits.DELETE | PermissionBits.VIEW | PermissionBits.EDIT, }); await AclEntry.create({ principalType: PrincipalType.USER, principalId: deletingUserId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: (multiAgent as unknown as { _id: mongoose.Types.ObjectId })._id, - permBits: PermissionBits.DELETE | PermissionBits.READ | PermissionBits.WRITE, + permBits: PermissionBits.DELETE | PermissionBits.VIEW | PermissionBits.EDIT, }); await AclEntry.create({ principalType: PrincipalType.USER, principalId: otherOwnerId, + principalModel: PrincipalModel.USER, resourceType: ResourceType.AGENT, resourceId: (multiAgent as unknown as { _id: mongoose.Types.ObjectId })._id, - permBits: PermissionBits.DELETE | PermissionBits.READ | PermissionBits.WRITE, + permBits: PermissionBits.DELETE | PermissionBits.VIEW | PermissionBits.EDIT, }); await deleteUserAgents(deletingUserId.toString()); diff --git a/packages/data-schemas/src/methods/agent.ts b/packages/data-schemas/src/methods/agent.ts index 43147eaea9..f4371dd15c 100644 --- a/packages/data-schemas/src/methods/agent.ts +++ b/packages/data-schemas/src/methods/agent.ts @@ -1,8 +1,8 @@ import crypto from 'node:crypto'; -import type { FilterQuery, Model, Types } from 'mongoose'; import { Constants, ResourceType, actionDelimiter } from 'librechat-data-provider'; +import type { FilterQuery, Model, Types } from 'mongoose'; +import type { IAgent, IAclEntry } from '~/types'; import logger from '~/config/winston'; -import type { IAgent } from '~/types'; const { mcp_delimiter } = Constants; @@ -525,7 +525,7 @@ export function createAgentMethods(mongoose: typeof import('mongoose'), deps: Ag */ async function deleteUserAgents(userId: string): Promise { const Agent = mongoose.models.Agent as Model; - const AclEntry = mongoose.models.AclEntry as Model; + const AclEntry = mongoose.models.AclEntry as Model; const User = mongoose.models.User as Model; try { @@ -546,9 +546,7 @@ export function createAgentMethods(mongoose: typeof import('mongoose'), deps: Ag .select('resourceId') .lean() : []; - const migratedIds = new Set( - (migratedEntries as Array<{ resourceId: Types.ObjectId }>).map((e) => e.resourceId.toString()), - ); + const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString())); const legacyAgents = authoredAgents.filter((a) => !migratedIds.has(a._id.toString())); const soleOwnedAgents = diff --git a/packages/data-schemas/src/methods/prompt.ts b/packages/data-schemas/src/methods/prompt.ts index 4edfc9f408..8fa8fd1a53 100644 --- a/packages/data-schemas/src/methods/prompt.ts +++ b/packages/data-schemas/src/methods/prompt.ts @@ -1,6 +1,6 @@ -import type { Model, Types } from 'mongoose'; import { ResourceType, SystemCategories } from 'librechat-data-provider'; -import type { IPrompt, IPromptGroup, IPromptGroupDocument } from '~/types'; +import type { Model, Types } from 'mongoose'; +import type { IAclEntry, IPrompt, IPromptGroup, IPromptGroupDocument } from '~/types'; import { escapeRegExp } from '~/utils/string'; import logger from '~/config/winston'; @@ -544,7 +544,7 @@ export function createPromptMethods(mongoose: typeof import('mongoose'), deps: P try { const PromptGroup = mongoose.models.PromptGroup as Model; const Prompt = mongoose.models.Prompt as Model; - const AclEntry = mongoose.models.AclEntry; + const AclEntry = mongoose.models.AclEntry as Model; const userObjectId = new ObjectId(userId); const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.PROMPTGROUP); @@ -561,12 +561,8 @@ export function createPromptMethods(mongoose: typeof import('mongoose'), deps: P .select('resourceId') .lean() : []; - const migratedIds = new Set( - (migratedEntries as Array<{ resourceId: Types.ObjectId }>).map((e) => e.resourceId.toString()), - ); - const legacyGroupIds = authoredGroupIds.filter( - (id) => !migratedIds.has(id.toString()), - ); + const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString())); + const legacyGroupIds = authoredGroupIds.filter((id) => !migratedIds.has(id.toString())); const allGroupIdsToDelete = [...soleOwnedIds, ...legacyGroupIds]; From a78865b5e0569d91c2cf3db9a97a218e54edb5d5 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 21 Mar 2026 14:45:53 -0400 Subject: [PATCH 106/111] =?UTF-8?q?=F0=9F=94=84=20refactor:=20Update=20Tok?= =?UTF-8?q?en=20Deletion=20Logic=20to=20Use=20AND=20Semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored the `deleteTokens` method to delete tokens based on all provided fields using AND conditions instead of OR, enhancing precision in token management. - Updated related tests to reflect the new logic, ensuring that only tokens matching all specified criteria are deleted. - Added new test cases for deleting tokens by type and userId, and for preventing cross-user token deletions, improving overall test coverage and robustness. - Introduced a new `type` field in the `TokenQuery` interface to support the updated deletion functionality. --- .../data-schemas/src/methods/token.spec.ts | 84 +++++++++++++++++-- packages/data-schemas/src/methods/token.ts | 16 ++-- packages/data-schemas/src/types/token.ts | 1 + 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/packages/data-schemas/src/methods/token.spec.ts b/packages/data-schemas/src/methods/token.spec.ts index e6cf56d18d..87c3916bf8 100644 --- a/packages/data-schemas/src/methods/token.spec.ts +++ b/packages/data-schemas/src/methods/token.spec.ts @@ -566,26 +566,94 @@ describe('Token Methods - Detailed Tests', () => { expect(remainingTokens).toHaveLength(3); }); - test('should delete multiple tokens when they match OR conditions', async () => { - // Create tokens that will match multiple conditions + test('should only delete tokens matching ALL provided fields (AND semantics)', async () => { await Token.create({ - token: 'multi-match', - userId: user2Id, // Will match userId condition + token: 'extra-user2-token', + userId: user2Id, email: 'different@example.com', createdAt: new Date(), expiresAt: new Date(Date.now() + 3600000), }); const result = await methods.deleteTokens({ - token: 'verify-token-1', + token: 'verify-token-2', userId: user2Id.toString(), }); - // Should delete: verify-token-1 (by token) + verify-token-2 (by userId) + multi-match (by userId) - expect(result.deletedCount).toBe(3); + expect(result.deletedCount).toBe(1); const remainingTokens = await Token.find({}); - expect(remainingTokens).toHaveLength(2); + expect(remainingTokens).toHaveLength(4); + expect(remainingTokens.find((t) => t.token === 'verify-token-1')).toBeDefined(); + expect(remainingTokens.find((t) => t.token === 'extra-user2-token')).toBeDefined(); + }); + + test('should delete tokens by type and userId together', async () => { + await Token.create([ + { + token: 'mcp-oauth-token', + userId: oauthUserId, + type: 'mcp_oauth', + identifier: 'mcp:test-server', + createdAt: new Date(), + expiresAt: new Date(Date.now() + 3600000), + }, + { + token: 'mcp-refresh-token', + userId: oauthUserId, + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-server:refresh', + createdAt: new Date(), + expiresAt: new Date(Date.now() + 3600000), + }, + ]); + + const result = await methods.deleteTokens({ + userId: oauthUserId.toString(), + type: 'mcp_oauth', + identifier: 'mcp:test-server', + }); + + expect(result.deletedCount).toBe(1); + + const remaining = await Token.find({ userId: oauthUserId }); + expect(remaining).toHaveLength(2); + expect(remaining.find((t) => t.type === 'mcp_oauth')).toBeUndefined(); + expect(remaining.find((t) => t.type === 'mcp_oauth_refresh')).toBeDefined(); + }); + + test('should not delete cross-user tokens with matching identifier', async () => { + const userAId = new mongoose.Types.ObjectId(); + const userBId = new mongoose.Types.ObjectId(); + + await Token.create([ + { + token: 'user-a-mcp', + userId: userAId, + type: 'mcp_oauth', + identifier: 'mcp:shared-server', + createdAt: new Date(), + expiresAt: new Date(Date.now() + 3600000), + }, + { + token: 'user-b-mcp', + userId: userBId, + type: 'mcp_oauth', + identifier: 'mcp:shared-server', + createdAt: new Date(), + expiresAt: new Date(Date.now() + 3600000), + }, + ]); + + await methods.deleteTokens({ + userId: userAId.toString(), + type: 'mcp_oauth', + identifier: 'mcp:shared-server', + }); + + const userBTokens = await Token.find({ userId: userBId }); + expect(userBTokens).toHaveLength(1); + expect(userBTokens[0].token).toBe('user-b-mcp'); }); test('should throw error when no query parameters provided', async () => { diff --git a/packages/data-schemas/src/methods/token.ts b/packages/data-schemas/src/methods/token.ts index 95fb57e426..a5de3d2a5d 100644 --- a/packages/data-schemas/src/methods/token.ts +++ b/packages/data-schemas/src/methods/token.ts @@ -48,10 +48,7 @@ export function createTokenMethods(mongoose: typeof import('mongoose')) { } } - /** - * Deletes all Token documents that match the provided token, user ID, or email. - * Email is automatically normalized to lowercase for case-insensitive matching. - */ + /** Deletes all Token documents matching every provided field (AND semantics). */ async function deleteTokens(query: TokenQuery): Promise { try { const Token = mongoose.models.Token; @@ -66,19 +63,19 @@ export function createTokenMethods(mongoose: typeof import('mongoose')) { if (query.email !== undefined) { conditions.push({ email: query.email.trim().toLowerCase() }); } + if (query.type !== undefined) { + conditions.push({ type: query.type }); + } if (query.identifier !== undefined) { conditions.push({ identifier: query.identifier }); } - /** - * If no conditions are specified, throw an error to prevent accidental deletion of all tokens - */ if (conditions.length === 0) { throw new Error('At least one query parameter must be provided'); } return await Token.deleteMany({ - $or: conditions, + $and: conditions, }); } catch (error) { logger.debug('An error occurred while deleting tokens:', error); @@ -104,6 +101,9 @@ export function createTokenMethods(mongoose: typeof import('mongoose')) { if (query.email) { conditions.push({ email: query.email.trim().toLowerCase() }); } + if (query.type) { + conditions.push({ type: query.type }); + } if (query.identifier) { conditions.push({ identifier: query.identifier }); } diff --git a/packages/data-schemas/src/types/token.ts b/packages/data-schemas/src/types/token.ts index 063e18a7c9..fd5bfa3a8a 100644 --- a/packages/data-schemas/src/types/token.ts +++ b/packages/data-schemas/src/types/token.ts @@ -26,6 +26,7 @@ export interface TokenQuery { userId?: Types.ObjectId | string; token?: string; email?: string; + type?: string; identifier?: string; } From 04e65bb21adf068a25f8c417c8d6337fdadfd4fa Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 21 Mar 2026 15:20:15 -0400 Subject: [PATCH 107/111] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Move=20direct?= =?UTF-8?q?=20model=20usage=20from=20PermissionsController=20to=20data-sch?= =?UTF-8?q?emas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/PermissionsController.js | 51 +++++------------- .../__tests__/PermissionsController.spec.js | 52 +++++-------------- packages/data-schemas/src/methods/agent.ts | 24 ++++++++- 3 files changed, 47 insertions(+), 80 deletions(-) diff --git a/api/server/controllers/PermissionsController.js b/api/server/controllers/PermissionsController.js index 59732572c0..1f200fce83 100644 --- a/api/server/controllers/PermissionsController.js +++ b/api/server/controllers/PermissionsController.js @@ -15,19 +15,11 @@ const { ensurePrincipalExists, getAvailableRoles, } = require('~/server/services/PermissionService'); -const { - searchPrincipals: searchLocalPrincipals, - sortPrincipalsByRelevance, - calculateRelevanceScore, - findRoleByIdentifier, - aggregateAclEntries, - bulkWriteAclEntries, -} = require('~/models'); const { entraIdPrincipalFeatureEnabled, searchEntraIdPrincipals, } = require('~/server/services/GraphApiService'); -const { Agent, AclEntry, AccessRole, User } = require('~/db/models'); +const db = require('~/models'); /** * Generic controller for resource permission endpoints @@ -46,28 +38,6 @@ const validateResourceType = (resourceType) => { } }; -/** - * Removes an agent from the favorites of specified users (fire-and-forget). - * Both AGENT and REMOTE_AGENT resource types share the Agent collection. - * @param {string} resourceId - The agent's MongoDB ObjectId hex string - * @param {string[]} userIds - User ObjectId strings whose favorites should be cleaned - */ -const removeRevokedAgentFromFavorites = (resourceId, userIds) => - Agent.findOne({ _id: resourceId }, { id: 1 }) - .lean() - .then((agent) => { - if (!agent) { - return; - } - return User.updateMany( - { _id: { $in: userIds }, 'favorites.agentId': agent.id }, - { $pull: { favorites: { agentId: agent.id } } }, - ); - }) - .catch((err) => { - logger.error('[removeRevokedAgentFromFavorites] Error cleaning up favorites', err); - }); - /** * Bulk update permissions for a resource (grant, update, remove) * @route PUT /api/{resourceType}/{resourceId}/permissions @@ -187,7 +157,9 @@ const updateResourcePermissions = async (req, res) => { .map((p) => p.id); if (isAgentResource && revokedUserIds.length > 0) { - removeRevokedAgentFromFavorites(resourceId, revokedUserIds); + db.removeAgentFromUserFavorites(resourceId, revokedUserIds).catch((err) => { + logger.error('[removeRevokedAgentFromFavorites] Error cleaning up favorites', err); + }); } /** @type {TUpdateResourcePermissionsResponse} */ @@ -220,7 +192,7 @@ const getResourcePermissions = async (req, res) => { const { resourceType, resourceId } = req.params; validateResourceType(resourceType); - const results = await aggregateAclEntries([ + const results = await db.aggregateAclEntries([ // Match ACL entries for this resource { $match: { @@ -317,9 +289,9 @@ const getResourcePermissions = async (req, res) => { if (resourceType === ResourceType.REMOTE_AGENT) { const enricherDeps = { - aggregateAclEntries, - bulkWriteAclEntries, - findRoleByIdentifier, + aggregateAclEntries: db.aggregateAclEntries, + bulkWriteAclEntries: db.bulkWriteAclEntries, + findRoleByIdentifier: db.findRoleByIdentifier, logger, }; const enrichResult = await enrichRemoteAgentPrincipals(enricherDeps, resourceId, principals); @@ -438,7 +410,7 @@ const searchPrincipals = async (req, res) => { typeFilters = validTypes.length > 0 ? validTypes : null; } - const localResults = await searchLocalPrincipals(query.trim(), searchLimit, typeFilters); + const localResults = await db.searchPrincipals(query.trim(), searchLimit, typeFilters); let allPrincipals = [...localResults]; const useEntraId = entraIdPrincipalFeatureEnabled(req.user); @@ -494,10 +466,11 @@ const searchPrincipals = async (req, res) => { } const scoredResults = allPrincipals.map((item) => ({ ...item, - _searchScore: calculateRelevanceScore(item, query.trim()), + _searchScore: db.calculateRelevanceScore(item, query.trim()), })); - const finalResults = sortPrincipalsByRelevance(scoredResults) + const finalResults = db + .sortPrincipalsByRelevance(scoredResults) .slice(0, searchLimit) .map((result) => { const { _searchScore, ...resultWithoutScore } = result; diff --git a/api/server/controllers/__tests__/PermissionsController.spec.js b/api/server/controllers/__tests__/PermissionsController.spec.js index 840eaf0c30..a8d9518455 100644 --- a/api/server/controllers/__tests__/PermissionsController.spec.js +++ b/api/server/controllers/__tests__/PermissionsController.spec.js @@ -29,10 +29,13 @@ jest.mock('~/server/services/PermissionService', () => ({ getResourcePermissionsMap: jest.fn(), })); +const mockRemoveAgentFromUserFavorites = jest.fn(); + jest.mock('~/models', () => ({ searchPrincipals: jest.fn(), sortPrincipalsByRelevance: jest.fn(), calculateRelevanceScore: jest.fn(), + removeAgentFromUserFavorites: (...args) => mockRemoveAgentFromUserFavorites(...args), })); jest.mock('~/server/services/GraphApiService', () => ({ @@ -40,20 +43,6 @@ jest.mock('~/server/services/GraphApiService', () => ({ searchEntraIdPrincipals: jest.fn(), })); -const mockAgentFindOne = jest.fn(); -const mockUserUpdateMany = jest.fn(); - -jest.mock('~/db/models', () => ({ - Agent: { - findOne: (...args) => mockAgentFindOne(...args), - }, - AclEntry: {}, - AccessRole: {}, - User: { - updateMany: (...args) => mockUserUpdateMany(...args), - }, -})); - const { updateResourcePermissions } = require('../PermissionsController'); const createMockReq = (overrides = {}) => ({ @@ -90,10 +79,7 @@ describe('PermissionsController', () => { errors: [], }); - mockAgentFindOne.mockReturnValue({ - lean: () => Promise.resolve({ _id: agentObjectId, id: 'agent_abc123' }), - }); - mockUserUpdateMany.mockResolvedValue({ modifiedCount: 1 }); + mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined); }); it('removes agent from revoked users favorites on AGENT resource type', async () => { @@ -111,11 +97,7 @@ describe('PermissionsController', () => { await flushPromises(); expect(res.status).toHaveBeenCalledWith(200); - expect(mockAgentFindOne).toHaveBeenCalledWith({ _id: agentObjectId }, { id: 1 }); - expect(mockUserUpdateMany).toHaveBeenCalledWith( - { _id: { $in: [revokedUserId] }, 'favorites.agentId': 'agent_abc123' }, - { $pull: { favorites: { agentId: 'agent_abc123' } } }, - ); + expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]); }); it('removes agent from revoked users favorites on REMOTE_AGENT resource type', async () => { @@ -132,8 +114,7 @@ describe('PermissionsController', () => { await updateResourcePermissions(req, res); await flushPromises(); - expect(mockAgentFindOne).toHaveBeenCalledWith({ _id: agentObjectId }, { id: 1 }); - expect(mockUserUpdateMany).toHaveBeenCalled(); + expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]); }); it('uses results.revoked (validated) not raw request payload', async () => { @@ -163,10 +144,7 @@ describe('PermissionsController', () => { await updateResourcePermissions(req, res); await flushPromises(); - expect(mockUserUpdateMany).toHaveBeenCalledWith( - expect.objectContaining({ _id: { $in: [validId] } }), - expect.any(Object), - ); + expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [validId]); }); it('skips cleanup when no USER principals are revoked', async () => { @@ -190,8 +168,7 @@ describe('PermissionsController', () => { await updateResourcePermissions(req, res); await flushPromises(); - expect(mockAgentFindOne).not.toHaveBeenCalled(); - expect(mockUserUpdateMany).not.toHaveBeenCalled(); + expect(mockRemoveAgentFromUserFavorites).not.toHaveBeenCalled(); }); it('skips cleanup for non-agent resource types', async () => { @@ -216,13 +193,11 @@ describe('PermissionsController', () => { await flushPromises(); expect(res.status).toHaveBeenCalledWith(200); - expect(mockAgentFindOne).not.toHaveBeenCalled(); + expect(mockRemoveAgentFromUserFavorites).not.toHaveBeenCalled(); }); it('handles agent not found gracefully', async () => { - mockAgentFindOne.mockReturnValue({ - lean: () => Promise.resolve(null), - }); + mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined); const req = createMockReq({ params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId }, @@ -237,13 +212,12 @@ describe('PermissionsController', () => { await updateResourcePermissions(req, res); await flushPromises(); - expect(mockAgentFindOne).toHaveBeenCalled(); - expect(mockUserUpdateMany).not.toHaveBeenCalled(); + expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(200); }); - it('logs error when User.updateMany fails without blocking response', async () => { - mockUserUpdateMany.mockRejectedValue(new Error('DB connection lost')); + it('logs error when removeAgentFromUserFavorites fails without blocking response', async () => { + mockRemoveAgentFromUserFavorites.mockRejectedValue(new Error('DB connection lost')); const req = createMockReq({ params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId }, diff --git a/packages/data-schemas/src/methods/agent.ts b/packages/data-schemas/src/methods/agent.ts index f4371dd15c..36d2d819cb 100644 --- a/packages/data-schemas/src/methods/agent.ts +++ b/packages/data-schemas/src/methods/agent.ts @@ -741,19 +741,39 @@ export function createAgentMethods(mongoose: typeof import('mongoose'), deps: Ag return await Agent.countDocuments({ is_promoted: true }); } + /** Removes an agent from the favorites of specified users. */ + async function removeAgentFromUserFavorites( + resourceId: string, + userIds: string[], + ): Promise { + const Agent = mongoose.models.Agent as Model; + const User = mongoose.models.User as Model; + + const agent = await Agent.findOne({ _id: resourceId }, { id: 1 }).lean(); + if (!agent) { + return; + } + + await User.updateMany( + { _id: { $in: userIds }, 'favorites.agentId': agent.id }, + { $pull: { favorites: { agentId: agent.id } } }, + ); + } + return { - createAgent, getAgent, getAgents, + createAgent, updateAgent, deleteAgent, deleteUserAgents, revertAgentVersion, countPromotedAgents, addAgentResourceFile, - removeAgentResourceFiles, getListAgentsByAccess, + removeAgentResourceFiles, generateActionMetadataHash, + removeAgentFromUserFavorites, }; } From 733a9364c006bee4680c966da0f38d178e8cee74 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:15:20 +0100 Subject: [PATCH 108/111] =?UTF-8?q?=F0=9F=8E=A8=20refactor:=20Redesign=20S?= =?UTF-8?q?idebar=20with=20Unified=20Icon=20Strip=20Layout=20(#12013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Graceful SidePanelContext handling when ChatContext unavailable The UnifiedSidebar component is rendered at the Root level before ChatContext is provided (which happens only in ChatRoute). This caused an error when useSidePanelContext tried to call useChatContext before it was available. Changes: - Made SidePanelProvider gracefully handle missing ChatContext with try/catch - Changed useSidePanelContext to return a safe default instead of throwing - Prevents render error on application load and improves robustness * fix: Provide default context value for ChatContext to prevent setFilesLoading errors The ChatContext was initialized with an empty object as default, causing 'setFilesLoading is not a function' errors when components tried to call functions from the context. This fix provides a proper default context with no-op functions for all expected properties. Fixes FileRow component errors that occurred when navigating to sections with file upload functionality (Agent Builder, Attach Files, etc.). * fix: Move ChatFormProvider to Root to fix Prompts sidebar rendering The ChatFormProvider was only wrapping ChatView, but the sidebar (including Prompts) renders separately and needs access to the ChatFormContext. ChatGroupItem uses useSubmitMessage which calls useChatFormContext, causing a React error when Prompts were accessed. This fix moves the ChatFormProvider to the Root component to wrap both the sidebar and the main chat view, ensuring the form context is available throughout the entire application. * fix: Active section switching and dead code cleanup Sync ActivePanelProvider state when defaultActive prop changes so clicking a collapsed-bar icon actually switches the expanded section. Remove the now-unused hideSidePanel atom and its Settings toggle. * style: Redesign sidebar layout with optimized spacing and positioning - Remove duplicate new chat button from sidebar, keep it in main header - Reposition account settings to bottom of expanded sidebar - Simplify collapsed bar padding and alignment - Clean up unused framer-motion imports from Header - Optimize vertical space usage in expanded panel - Align search bar icon color with sidebar theme * fix: Chat history not showing in sidebar Add h-full to ConversationsSection outer div so it fills the Nav content panel, giving react-virtualized's AutoSizer a measurable height. Change Nav content panel from overflow-y-auto to overflow-hidden since the virtualized list handles its own scrolling. * refactor: Move nav icons to fixed icon strip alongside sidebar toggle Extract section icons from the Nav content panel into the ExpandedPanel icon strip, matching the CollapsedBar layout. Both states now share identical button styling and 50px width, eliminating layout shift on toggle. Nav.tsx simplified to content-only rendering. Set text-text-primary on Nav content for consistent child text color. * refactor: sidebar components and remove unused NewChat component * refactor: streamline sidebar components and introduce NewChat button * refactor: enhance sidebar functionality with expanded state management and improved layout * fix: re-implement sidebar resizing functionality with mouse events * feat: enhance sidebar layout responsiveness on mobile * refactor: remove unused components and streamline sidebar functionality * feat: enhance sidebar behavior with responsive transformations for small screens * feat: add new chat button for small screens with message cache clearing * feat: improve state management in sidebar and marketplace components * feat: enhance scrolling behavior in AgentPanel and Nav components * fix: normalize sidebar panel font sizes and default panel selection Set text-sm as base font size on the shared Nav container so all panels render consistently. Guard against empty localStorage value when restoring the active sidebar panel. * fix: adjust avatar size and class for collapsed state in AccountSettings component * style: adjust padding and class names in Nav, Parameters, and ConversationsSection components * fix: close mobile sidebar on pinned favorite selection * refactor: remove unused key in translation file * fix: Address review findings for unified sidebar - Restore ChatFormProvider per-ChatView to fix multi-conversation input isolation; add separate ChatFormProvider in UnifiedSidebar for Prompts panel access - Add inert attribute on mobile sidebar (when collapsed) and main content (when sidebar overlay is open) to prevent keyboard focus leaking - Replace unsafe `as unknown as TChatContext` cast with null-based context that throws descriptively when used outside a provider - Throttle mousemove resize handler with requestAnimationFrame to prevent React state updates at 120Hz during sidebar drag - Unify active panel state: remove split between activeSection in UnifiedSidebar and internal state in ActivePanelContext; single source of truth with localStorage sync on every write - Delete orphaned SidePanelProvider/useSidePanelContext (no consumers after SidePanel.tsx removal) - Add data-testid="new-chat-button" to NewChat component - Add includeHidePanel option to useSideNavLinks; remove no-op hidePanel callback and post-hoc filter in useUnifiedSidebarLinks - Close sidebar on first mobile visit when localStorage has no prior state - Remove unnecessary min-width/max-width CSS transitions (only width needed) - Remove dead SideNav re-export from SidePanel/index.ts - Remove duplicate aria-label from Sidebar nav element - Fix trailing blank line in mobile.css * style: fix prettier formatting in Root.tsx * fix: Address remaining review findings and re-render isolation - Extract useChatHelpers(0) into SidebarChatProvider child component so Recoil atom subscriptions (streaming tokens, latestMessage, etc.) only re-render the active panel — not the sidebar shell, resize logic, or icon strip (Finding 4) - Fix prompt pre-fill when sidebar form context differs from chat form: useSubmitMessage now reads the actual textarea DOM value via mainTextareaId as fallback for the currentText newline check (Finding 1) - Add id="close-sidebar-button" and data-testid to ExpandedPanel toggle so OpenSidebar focus management works after expand (Finding 10/N3) - Replace Dispatch> prop with onResizeKeyboard(direction) callback on Sidebar (Finding 13) - Fix first-mobile-visit sidebar flash: atom default now checks window.matchMedia at init time instead of defaulting to true then correcting in a useEffect; removes eslint-disable suppression (N1/N2) - Add tests for ActivePanelContext and ChatContext (Finding 8) * refactor: remove no-op memo from SidebarChatProvider memo(SidebarChatProvider) provided no memoization because its only prop (children) is inline JSX — a new reference on every parent render. The streaming isolation works through Recoil subscription scoping, not memo. Clarified in the JSDoc comment. * fix: add shebang to pre-commit hook for Windows compatibility Git on Windows cannot spawn hook scripts without a shebang line, causing 'cannot spawn .husky/pre-commit: No such file or directory'. * style: fix sidebar panel styling inconsistencies - Remove inner overflow-y-auto from AgentPanel form to eliminate double scrollbars when nested inside Nav.tsx's scroll container - Add consistent padding (px-3 py-2) to Nav.tsx panel container - Remove hardcoded 150px cell widths from Files PanelTable; widen date column from 25% to 35% so dates are no longer cut off - Compact pagination row with flex-wrap and smaller text - Add px-1 padding to Parameters panel for consistency - Change overflow-x-visible to overflow-x-hidden on Files and Bookmarks panels to prevent horizontal overflow * fix: Restore panel styling regressions in unified sidebar - Nav.tsx wrapper: remove extra px-3 padding, add hide-scrollbar class, restore py-1 to match old ResizablePanel wrapper - AgentPanel: restore scrollbar-gutter-stable and mx-1 margin (was px-1) - Parameters/Panel: restore p-3 padding (was px-1 pt-1) - Files/Panel: restore overflow-x-visible (was hidden, clipping content) - Files/PanelTable: restore 75/25 column widths, restore 150px cell width constraints, restore pagination text-sm and gap-2 - Bookmarks/Panel: restore overflow-x-visible * style: initial improvements post-sidenav change * style: update text size in DynamicTextarea for improved readability * style: update FilterPrompts alignment in PromptsAccordion for better layout consistency * style: adjust component heights and padding for consistency across SidePanel elements - Updated height from 40px to 36px in AgentSelect for uniformity - Changed button size class from "bg-transparent" to "size-9" in BookmarkTable, MCPBuilderPanel, and MemoryPanel - Added padding class "py-1.5" in DynamicDropdown for improved spacing - Reduced height from 10 to 9 in DynamicInput and DynamicTags for a cohesive look - Adjusted padding in Parameters/Panel for better layout * style: standardize button sizes and icon dimensions across chat components - Updated button size class from 'size-10' to 'size-9' in multiple components for consistency. - Adjusted icon sizes from 'icon-lg' to 'icon-md' in various locations to maintain uniformity. - Modified header height for better alignment with design specifications. * style: enhance layout consistency and component structure across various panels - Updated ActivePanelContext to utilize useCallback and useMemo for improved performance. - Adjusted padding and layout in multiple SidePanel components for better visual alignment. - Standardized icon sizes and button dimensions in AddMultiConvo and other components. - Improved overall spacing and structure in PromptsAccordion, MemoryPanel, and FilesPanel for a cohesive design. * style: standardize component heights and text sizes across various panels - Adjusted button heights from 10px to 9px in multiple components for consistency. - Updated text sizes from 'text-sm' to 'text-xs' in DynamicCheckbox, DynamicCombobox, DynamicDropdown, DynamicInput, DynamicSlider, DynamicSwitch, DynamicTags, and DynamicTextarea for improved readability. - Added portal prop to account settings menu for better rendering behavior. * refactor: optimize Tooltip component structure for performance - Introduced a new memoized TooltipPopup component to prevent unnecessary re-renders of the TooltipAnchor when the tooltip mounts/unmounts. - Updated TooltipAnchor to utilize the new TooltipPopup, improving separation of concerns and enhancing performance. - Maintained existing functionality while improving code clarity and maintainability. * refactor: improve sidebar transition handling for better responsiveness - Enhanced the transition properties in the UnifiedSidebar component to include min-width and max-width adjustments, ensuring smoother resizing behavior. - Cleaned up import statements by removing unnecessary lines for better code clarity. * fix: prevent text selection during sidebar resizing - Added functionality to disable text selection on the body while the sidebar is being resized, enhancing user experience during interactions. - Restored text selection capability once resizing is complete, ensuring normal behavior resumes. * fix: ensure Header component is always rendered in ChatView - Removed conditional rendering of the Header component to ensure it is always displayed, improving the consistency of the chat interface. * refactor: add NewChatButton to ExpandedPanel for improved user interaction - Introduced a NewChatButton component in the ExpandedPanel, allowing users to initiate new conversations easily. - Implemented functionality to clear message cache and invalidate queries upon button click, enhancing performance and user experience. - Restored import statements for OpenSidebar and PresetsMenu in Header component for better organization. --------- Co-authored-by: Danny Avila --- .husky/pre-commit | 1 + client/src/Providers/ActivePanelContext.tsx | 36 +- client/src/Providers/ChatContext.tsx | 10 +- client/src/Providers/SidePanelContext.tsx | 31 -- .../__tests__/ActivePanelContext.spec.tsx | 60 +++ .../Providers/__tests__/ChatContext.spec.tsx | 31 ++ client/src/Providers/index.ts | 1 - client/src/common/types.ts | 12 - client/src/components/Agents/Marketplace.tsx | 383 +++++++----------- .../components/Agents/MarketplaceContext.tsx | 2 +- client/src/components/Chat/AddMultiConvo.tsx | 4 +- client/src/components/Chat/ChatView.tsx | 12 +- .../components/Chat/ExportAndShareMenu.tsx | 4 +- client/src/components/Chat/Header.tsx | 30 +- .../components/Chat/Menus/BookmarkMenu.tsx | 6 +- .../Chat/Menus/Endpoints/ModelSelector.tsx | 2 +- .../components/Chat/Menus/HeaderNewChat.tsx | 42 -- .../src/components/Chat/Menus/OpenSidebar.tsx | 23 +- .../src/components/Chat/Menus/PresetsMenu.tsx | 4 +- client/src/components/Chat/Menus/index.ts | 1 - client/src/components/Chat/Presentation.tsx | 37 +- client/src/components/Chat/TemporaryChat.tsx | 4 +- .../Conversations/Conversations.tsx | 4 +- client/src/components/Nav/AccountSettings.tsx | 34 +- .../components/Nav/Bookmarks/BookmarkNav.tsx | 4 +- .../Nav/Favorites/FavoritesList.tsx | 12 +- client/src/components/Nav/MobileNav.tsx | 90 ---- client/src/components/Nav/Nav.tsx | 307 -------------- client/src/components/Nav/NewChat.tsx | 128 ++---- client/src/components/Nav/SearchBar.tsx | 2 +- .../Nav/SettingsTabs/General/General.tsx | 7 - client/src/components/Nav/index.ts | 2 - .../components/Prompts/PromptsAccordion.tsx | 9 +- .../SidePanel/Agents/ActionsInput.tsx | 6 +- .../SidePanel/Agents/Advanced/AgentChain.tsx | 6 +- .../Agents/Advanced/AgentHandoffs.tsx | 4 +- .../SidePanel/Agents/AgentConfig.tsx | 10 +- .../SidePanel/Agents/AgentPanel.tsx | 4 +- .../SidePanel/Agents/AgentPanelSkeleton.tsx | 6 +- .../SidePanel/Agents/AgentSelect.tsx | 2 +- .../components/SidePanel/Agents/Artifacts.tsx | 4 +- .../SidePanel/Agents/Code/Action.tsx | 2 +- .../SidePanel/Agents/Code/Files.tsx | 2 +- .../components/SidePanel/Agents/Code/Form.tsx | 2 +- .../SidePanel/Agents/FileContext.tsx | 6 +- .../SidePanel/Agents/FileSearch.tsx | 6 +- .../SidePanel/Agents/FileSearchCheckbox.tsx | 2 +- .../SidePanel/Agents/Instructions.tsx | 5 +- .../components/SidePanel/Agents/MCPTools.tsx | 2 +- .../SidePanel/Agents/ModelPanel.tsx | 10 +- .../SidePanel/Agents/Search/Action.tsx | 2 +- .../SidePanel/Agents/Search/Form.tsx | 2 +- .../SidePanel/Bookmarks/BookmarkTable.tsx | 4 +- .../src/components/SidePanel/Files/Panel.tsx | 2 +- .../SidePanel/Files/PanelColumns.tsx | 5 +- .../SidePanel/Files/PanelFileCell.tsx | 2 +- .../components/SidePanel/Files/PanelTable.tsx | 23 +- .../SidePanel/MCPBuilder/MCPBuilderPanel.tsx | 6 +- .../SidePanel/Memories/MemoryPanel.tsx | 8 +- client/src/components/SidePanel/Nav.tsx | 118 +----- .../SidePanel/Parameters/DynamicCheckbox.tsx | 2 +- .../SidePanel/Parameters/DynamicCombobox.tsx | 2 +- .../SidePanel/Parameters/DynamicDropdown.tsx | 3 +- .../SidePanel/Parameters/DynamicInput.tsx | 4 +- .../SidePanel/Parameters/DynamicSlider.tsx | 2 +- .../SidePanel/Parameters/DynamicSwitch.tsx | 2 +- .../SidePanel/Parameters/DynamicTags.tsx | 6 +- .../SidePanel/Parameters/DynamicTextarea.tsx | 7 +- .../components/SidePanel/Parameters/Panel.tsx | 2 +- client/src/components/SidePanel/SidePanel.tsx | 194 --------- .../components/SidePanel/SidePanelGroup.tsx | 191 +++------ client/src/components/SidePanel/index.ts | 1 - .../UnifiedSidebar/ConversationsSection.tsx | 170 ++++++++ .../UnifiedSidebar/ExpandedPanel.tsx | 169 ++++++++ .../src/components/UnifiedSidebar/Sidebar.tsx | 65 +++ .../UnifiedSidebar/UnifiedSidebar.tsx | 206 ++++++++++ client/src/components/UnifiedSidebar/index.ts | 2 + client/src/hooks/Messages/useSubmitMessage.ts | 4 +- client/src/hooks/Nav/useSideNavLinks.ts | 21 +- .../src/hooks/Nav/useUnifiedSidebarLinks.ts | 65 +++ client/src/locales/en/translation.json | 2 - client/src/mobile.css | 40 -- client/src/routes/Root.tsx | 39 +- client/src/store/settings.ts | 5 +- .../client/src/components/ControlCombobox.tsx | 2 +- .../client/src/components/FilterInput.tsx | 2 +- packages/client/src/components/Tooltip.tsx | 104 +++-- packages/client/src/svgs/NewChatIcon.tsx | 5 +- 88 files changed, 1310 insertions(+), 1593 deletions(-) delete mode 100644 client/src/Providers/SidePanelContext.tsx create mode 100644 client/src/Providers/__tests__/ActivePanelContext.spec.tsx create mode 100644 client/src/Providers/__tests__/ChatContext.spec.tsx delete mode 100644 client/src/components/Chat/Menus/HeaderNewChat.tsx delete mode 100644 client/src/components/Nav/MobileNav.tsx delete mode 100644 client/src/components/Nav/Nav.tsx delete mode 100644 client/src/components/SidePanel/SidePanel.tsx create mode 100644 client/src/components/UnifiedSidebar/ConversationsSection.tsx create mode 100644 client/src/components/UnifiedSidebar/ExpandedPanel.tsx create mode 100644 client/src/components/UnifiedSidebar/Sidebar.tsx create mode 100644 client/src/components/UnifiedSidebar/UnifiedSidebar.tsx create mode 100644 client/src/components/UnifiedSidebar/index.ts create mode 100644 client/src/hooks/Nav/useUnifiedSidebarLinks.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 23c736d1de..70fef90065 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ +#!/bin/sh [ -n "$CI" ] && exit 0 npx lint-staged --config ./.husky/lint-staged.config.js diff --git a/client/src/Providers/ActivePanelContext.tsx b/client/src/Providers/ActivePanelContext.tsx index 4a8d6ccfc4..9d6082d4e4 100644 --- a/client/src/Providers/ActivePanelContext.tsx +++ b/client/src/Providers/ActivePanelContext.tsx @@ -1,31 +1,31 @@ -import { createContext, useContext, useState, ReactNode } from 'react'; +import { createContext, useCallback, useContext, useMemo, useState, ReactNode } from 'react'; + +const STORAGE_KEY = 'side:active-panel'; +const DEFAULT_PANEL = 'conversations'; + +function getInitialActivePanel(): string { + const saved = localStorage.getItem(STORAGE_KEY); + return saved ? saved : DEFAULT_PANEL; +} interface ActivePanelContextType { - active: string | undefined; + active: string; setActive: (id: string) => void; } const ActivePanelContext = createContext(undefined); -export function ActivePanelProvider({ - children, - defaultActive, -}: { - children: ReactNode; - defaultActive?: string; -}) { - const [active, _setActive] = useState(defaultActive); +export function ActivePanelProvider({ children }: { children: ReactNode }) { + const [active, _setActive] = useState(getInitialActivePanel); - const setActive = (id: string) => { - localStorage.setItem('side:active-panel', id); + const setActive = useCallback((id: string) => { + localStorage.setItem(STORAGE_KEY, id); _setActive(id); - }; + }, []); - return ( - - {children} - - ); + const value = useMemo(() => ({ active, setActive }), [active, setActive]); + + return {children}; } export function useActivePanel() { diff --git a/client/src/Providers/ChatContext.tsx b/client/src/Providers/ChatContext.tsx index 3d3acbcc42..8af75f90c0 100644 --- a/client/src/Providers/ChatContext.tsx +++ b/client/src/Providers/ChatContext.tsx @@ -2,5 +2,11 @@ import { createContext, useContext } from 'react'; import useChatHelpers from '~/hooks/Chat/useChatHelpers'; type TChatContext = ReturnType; -export const ChatContext = createContext({} as TChatContext); -export const useChatContext = () => useContext(ChatContext); +export const ChatContext = createContext(null); +export const useChatContext = () => { + const ctx = useContext(ChatContext); + if (!ctx) { + throw new Error('useChatContext must be used within a ChatContext.Provider'); + } + return ctx; +}; diff --git a/client/src/Providers/SidePanelContext.tsx b/client/src/Providers/SidePanelContext.tsx deleted file mode 100644 index 3ce7834ccc..0000000000 --- a/client/src/Providers/SidePanelContext.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { createContext, useContext, useMemo } from 'react'; -import type { EModelEndpoint } from 'librechat-data-provider'; -import { useChatContext } from './ChatContext'; - -interface SidePanelContextValue { - endpoint?: EModelEndpoint | null; -} - -const SidePanelContext = createContext(undefined); - -export function SidePanelProvider({ children }: { children: React.ReactNode }) { - const { conversation } = useChatContext(); - - /** Context value only created when endpoint changes */ - const contextValue = useMemo( - () => ({ - endpoint: conversation?.endpoint, - }), - [conversation?.endpoint], - ); - - return {children}; -} - -export function useSidePanelContext() { - const context = useContext(SidePanelContext); - if (!context) { - throw new Error('useSidePanelContext must be used within SidePanelProvider'); - } - return context; -} diff --git a/client/src/Providers/__tests__/ActivePanelContext.spec.tsx b/client/src/Providers/__tests__/ActivePanelContext.spec.tsx new file mode 100644 index 0000000000..6a6059c9b4 --- /dev/null +++ b/client/src/Providers/__tests__/ActivePanelContext.spec.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { ActivePanelProvider, useActivePanel } from '~/Providers/ActivePanelContext'; + +const STORAGE_KEY = 'side:active-panel'; + +function TestConsumer() { + const { active, setActive } = useActivePanel(); + return ( +
+ {active} +
+ ); +} + +describe('ActivePanelContext', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('defaults to conversations when no localStorage value exists', () => { + render( + + + , + ); + expect(screen.getByTestId('active')).toHaveTextContent('conversations'); + }); + + it('reads initial value from localStorage', () => { + localStorage.setItem(STORAGE_KEY, 'memories'); + render( + + + , + ); + expect(screen.getByTestId('active')).toHaveTextContent('memories'); + }); + + it('setActive updates state and writes to localStorage', () => { + render( + + + , + ); + fireEvent.click(screen.getByTestId('switch-btn')); + expect(screen.getByTestId('active')).toHaveTextContent('bookmarks'); + expect(localStorage.getItem(STORAGE_KEY)).toBe('bookmarks'); + }); + + it('throws when useActivePanel is called outside provider', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => render()).toThrow( + 'useActivePanel must be used within an ActivePanelProvider', + ); + spy.mockRestore(); + }); +}); diff --git a/client/src/Providers/__tests__/ChatContext.spec.tsx b/client/src/Providers/__tests__/ChatContext.spec.tsx new file mode 100644 index 0000000000..0ed00bf580 --- /dev/null +++ b/client/src/Providers/__tests__/ChatContext.spec.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { ChatContext, useChatContext } from '~/Providers/ChatContext'; + +function TestConsumer() { + const ctx = useChatContext(); + return {ctx.index}; +} + +describe('ChatContext', () => { + it('throws when useChatContext is called outside a provider', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => render()).toThrow( + 'useChatContext must be used within a ChatContext.Provider', + ); + spy.mockRestore(); + }); + + it('provides context value when wrapped in provider', () => { + const mockHelpers = { index: 0 } as ReturnType< + typeof import('~/hooks/Chat/useChatHelpers').default + >; + render( + + + , + ); + expect(screen.getByTestId('index')).toHaveTextContent('0'); + }); +}); diff --git a/client/src/Providers/index.ts b/client/src/Providers/index.ts index 43a16fa976..3ae90e189c 100644 --- a/client/src/Providers/index.ts +++ b/client/src/Providers/index.ts @@ -22,7 +22,6 @@ export * from './ToolCallsMapContext'; export * from './SetConvoContext'; export * from './SearchContext'; export * from './BadgeRowContext'; -export * from './SidePanelContext'; export * from './DragDropContext'; export * from './ArtifactsContext'; export * from './PromptGroupsContext'; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index d47ff02bd8..85044bb2bc 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -132,13 +132,6 @@ export type NavLink = { id: string; }; -export interface NavProps { - isCollapsed: boolean; - links: NavLink[]; - resize?: (size: number) => void; - defaultActive?: string; -} - export interface DataColumnMeta { meta: | { @@ -561,11 +554,6 @@ export interface ModelItemProps { className?: string; } -export type ContextType = { - navVisible: boolean; - setNavVisible: React.Dispatch>; -}; - export interface SwitcherProps { endpoint?: t.EModelEndpoint | null; endpointKeyProvided: boolean; diff --git a/client/src/components/Agents/Marketplace.tsx b/client/src/components/Agents/Marketplace.tsx index 69db9fc630..0c9c9fb4cc 100644 --- a/client/src/components/Agents/Marketplace.tsx +++ b/client/src/components/Agents/Marketplace.tsx @@ -1,23 +1,17 @@ import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; -import { useRecoilState } from 'recoil'; -import { useOutletContext } from 'react-router-dom'; -import { useQueryClient } from '@tanstack/react-query'; import { useSearchParams, useParams, useNavigate } from 'react-router-dom'; -import { TooltipAnchor, Button, NewChatIcon, useMediaQuery } from '@librechat/client'; -import { PermissionTypes, Permissions, QueryKeys } from 'librechat-data-provider'; +import { useMediaQuery } from '@librechat/client'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; -import type { ContextType } from '~/common'; import { useDocumentTitle, useHasAccess, useLocalize, TranslationKeys } from '~/hooks'; import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider'; import MarketplaceAdminSettings from './MarketplaceAdminSettings'; -import { SidePanelProvider, useChatContext } from '~/Providers'; import { SidePanelGroup } from '~/components/SidePanel'; -import { OpenSidebar } from '~/components/Chat/Menus'; -import { cn, clearMessagesCache } from '~/utils'; +import { NewChat } from '~/components/Nav'; +import { cn } from '~/utils'; import CategoryTabs from './CategoryTabs'; import SearchBar from './SearchBar'; import AgentGrid from './AgentGrid'; -import store from '~/store'; interface AgentMarketplaceProps { className?: string; @@ -34,13 +28,9 @@ const AgentMarketplace: React.FC = ({ className = '' }) = const localize = useLocalize(); const navigate = useNavigate(); const { category } = useParams(); - const queryClient = useQueryClient(); const [searchParams, setSearchParams] = useSearchParams(); - const { conversation, newConversation } = useChatContext(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); - const { navVisible, setNavVisible } = useOutletContext(); - const [hideSidePanel, setHideSidePanel] = useRecoilState(store.hideSidePanel); // Get URL parameters const searchQuery = searchParams.get('q') || ''; @@ -59,15 +49,6 @@ const AgentMarketplace: React.FC = ({ className = '' }) = // Set page title useDocumentTitle(`${localize('com_agents_marketplace')} | LibreChat`); - // Ensure right sidebar is always visible in marketplace - useEffect(() => { - setHideSidePanel(false); - - // Also try to force expand via localStorage - localStorage.setItem('hideSidePanel', 'false'); - localStorage.setItem('fullPanelCollapse', 'false'); - }, [setHideSidePanel, hideSidePanel]); - // Ensure endpoints config is loaded first (required for agent queries) useGetEndpointsQuery(); @@ -193,33 +174,6 @@ const AgentMarketplace: React.FC = ({ className = '' }) = } }; - /** - * Handle new chat button click - */ - - const handleNewChat = (e: React.MouseEvent) => { - if (e.button === 0 && (e.ctrlKey || e.metaKey)) { - window.open('/c/new', '_blank'); - return; - } - clearMessagesCache(queryClient, conversation?.conversationId); - queryClient.invalidateQueries([QueryKeys.messages]); - newConversation(); - }; - - // Layout configuration for SidePanelGroup - const defaultLayout = useMemo(() => { - const resizableLayout = localStorage.getItem('react-resizable-panels:layout'); - return typeof resizableLayout === 'string' ? JSON.parse(resizableLayout) : undefined; - }, []); - - const defaultCollapsed = useMemo(() => { - const collapsedPanels = localStorage.getItem('react-resizable-panels:collapsed'); - return typeof collapsedPanels === 'string' ? JSON.parse(collapsedPanels) : true; - }, []); - - const fullCollapse = useMemo(() => localStorage.getItem('fullPanelCollapse') === 'true', []); - const hasAccessToMarketplace = useHasAccess({ permissionType: PermissionTypes.MARKETPLACE, permission: Permissions.USE, @@ -241,99 +195,144 @@ const AgentMarketplace: React.FC = ({ className = '' }) = } return (
- - -
- {/* Scrollable container */} -
- {/* Simplified header for agents marketplace - only show nav controls when needed */} - {!isSmallScreen && ( -
-
- {!navVisible ? ( - <> - - - - - } - /> - - ) : ( - // Invisible placeholder to maintain height -
- )} -
-
- )} - {/* Hero Section - scrolls away */} - {!isSmallScreen && ( -
-
-

- {localize('com_agents_marketplace')} -

-

- {localize('com_agents_marketplace_subtitle')} -

-
-
- )} - {/* Sticky wrapper for search bar and categories */} -
-
- {/* Search bar */} -
- - {/* TODO: Remove this once we have a better way to handle admin settings */} - {/* Admin Settings */} - -
- - {/* Category tabs */} - + +
+ {/* Scrollable container */} +
+ {/* Simplified header for agents marketplace - only show nav controls when needed */} + {!isSmallScreen && ( +
+ +
+ )} + {/* Hero Section - scrolls away */} + {!isSmallScreen && ( +
+
+

+ {localize('com_agents_marketplace')} +

+

+ {localize('com_agents_marketplace_subtitle')} +

- {/* Scrollable content area */} -
- {/* Two-pane animated container wrapping category header + grid */} -
- {/* Current content pane */} + )} + {/* Sticky wrapper for search bar and categories */} +
+
+ {/* Search bar */} +
+ + {/* TODO: Remove this once we have a better way to handle admin settings */} + {/* Admin Settings */} + +
+ + {/* Category tabs */} + +
+
+ {/* Scrollable content area */} +
+ {/* Two-pane animated container wrapping category header + grid */} +
+ {/* Current content pane */} +
+ {/* Category header - only show when not searching */} + {!searchQuery && ( +
+ {(() => { + // Get category data for display + const getCategoryData = () => { + if (displayCategory === 'promoted') { + return { + name: localize('com_agents_top_picks'), + description: localize('com_agents_recommended'), + }; + } + if (displayCategory === 'all') { + return { + name: localize('com_agents_all'), + description: localize('com_agents_all_description'), + }; + } + + // Find the category in the API data + const categoryData = categoriesQuery.data?.find( + (cat) => cat.value === displayCategory, + ); + if (categoryData) { + return { + name: categoryData.label?.startsWith('com_') + ? localize(categoryData.label as TranslationKeys) + : categoryData.label, + description: categoryData.description?.startsWith('com_') + ? localize(categoryData.description as TranslationKeys) + : categoryData.description || '', + }; + } + + // Fallback for unknown categories + return { + name: + displayCategory.charAt(0).toUpperCase() + displayCategory.slice(1), + description: '', + }; + }; + + const { name, description } = getCategoryData(); + + return ( +
+

{name}

+ {description && ( +

{description}

+ )} +
+ ); + })()} +
+ )} + + {/* Agent grid */} + +
+ + {/* Next content pane, only during transition */} + {isTransitioning && nextCategory && (
{/* Category header - only show when not searching */} {!searchQuery && ( @@ -341,13 +340,13 @@ const AgentMarketplace: React.FC = ({ className = '' }) = {(() => { // Get category data for display const getCategoryData = () => { - if (displayCategory === 'promoted') { + if (nextCategory === 'promoted') { return { name: localize('com_agents_top_picks'), description: localize('com_agents_recommended'), }; } - if (displayCategory === 'all') { + if (nextCategory === 'all') { return { name: localize('com_agents_all'), description: localize('com_agents_all_description'), @@ -356,7 +355,7 @@ const AgentMarketplace: React.FC = ({ className = '' }) = // Find the category in the API data const categoryData = categoriesQuery.data?.find( - (cat) => cat.value === displayCategory, + (cat) => cat.value === nextCategory, ); if (categoryData) { return { @@ -364,7 +363,9 @@ const AgentMarketplace: React.FC = ({ className = '' }) = ? localize(categoryData.label as TranslationKeys) : categoryData.label, description: categoryData.description?.startsWith('com_') - ? localize(categoryData.description as TranslationKeys) + ? localize( + categoryData.description as Parameters[0], + ) : categoryData.description || '', }; } @@ -372,7 +373,8 @@ const AgentMarketplace: React.FC = ({ className = '' }) = // Fallback for unknown categories return { name: - displayCategory.charAt(0).toUpperCase() + displayCategory.slice(1), + (nextCategory || '').charAt(0).toUpperCase() + + (nextCategory || '').slice(1), description: '', }; }; @@ -393,102 +395,21 @@ const AgentMarketplace: React.FC = ({ className = '' }) = {/* Agent grid */}
+ )} - {/* Next content pane, only during transition */} - {isTransitioning && nextCategory && ( -
- {/* Category header - only show when not searching */} - {!searchQuery && ( -
- {(() => { - // Get category data for display - const getCategoryData = () => { - if (nextCategory === 'promoted') { - return { - name: localize('com_agents_top_picks'), - description: localize('com_agents_recommended'), - }; - } - if (nextCategory === 'all') { - return { - name: localize('com_agents_all'), - description: localize('com_agents_all_description'), - }; - } - - // Find the category in the API data - const categoryData = categoriesQuery.data?.find( - (cat) => cat.value === nextCategory, - ); - if (categoryData) { - return { - name: categoryData.label?.startsWith('com_') - ? localize(categoryData.label as TranslationKeys) - : categoryData.label, - description: categoryData.description?.startsWith('com_') - ? localize( - categoryData.description as Parameters[0], - ) - : categoryData.description || '', - }; - } - - // Fallback for unknown categories - return { - name: - (nextCategory || '').charAt(0).toUpperCase() + - (nextCategory || '').slice(1), - description: '', - }; - }; - - const { name, description } = getCategoryData(); - - return ( -
-

{name}

- {description && ( -

{description}

- )} -
- ); - })()} -
- )} - - {/* Agent grid */} - -
- )} - - {/* Note: Using Tailwind keyframes for slide in/out animations */} -
+ {/* Note: Using Tailwind keyframes for slide in/out animations */}
-
-
- +
+
+
); }; diff --git a/client/src/components/Agents/MarketplaceContext.tsx b/client/src/components/Agents/MarketplaceContext.tsx index 09c88e3291..9193cbb82b 100644 --- a/client/src/components/Agents/MarketplaceContext.tsx +++ b/client/src/components/Agents/MarketplaceContext.tsx @@ -13,5 +13,5 @@ interface MarketplaceProviderProps { export const MarketplaceProvider: React.FC = ({ children }) => { const chatHelpers = useChatHelpers(0, 'new'); - return {children}; + return {children}; }; diff --git a/client/src/components/Chat/AddMultiConvo.tsx b/client/src/components/Chat/AddMultiConvo.tsx index 48e9919092..101dbadd19 100644 --- a/client/src/components/Chat/AddMultiConvo.tsx +++ b/client/src/components/Chat/AddMultiConvo.tsx @@ -44,9 +44,9 @@ function AddMultiConvo() { aria-label={localize('com_ui_add_multi_conversation')} onClick={clickHandler} data-testid="add-multi-convo-button" - className="inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light bg-presentation text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary" + className="inline-flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-border-light bg-presentation text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary" > -